<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shubham</title>
    <description>The latest articles on DEV Community by Shubham (@shubham399).</description>
    <link>https://dev.to/shubham399</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F366471%2F5ee5ca23-114c-4498-86ed-33a9db44c8a9.png</url>
      <title>DEV Community: Shubham</title>
      <link>https://dev.to/shubham399</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shubham399"/>
    <language>en</language>
    <item>
      <title>Type-Driven Security: Reducing OWASP Risk With Strong Types</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 26 Jul 2026 01:07:06 +0000</pubDate>
      <link>https://dev.to/shubham399/type-driven-security-reducing-owasp-risk-with-strong-types-3b3o</link>
      <guid>https://dev.to/shubham399/type-driven-security-reducing-owasp-risk-with-strong-types-3b3o</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fa5a9e3dd-9253-49a7-ac2e-8f6cab61ae90.png%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fa5a9e3dd-9253-49a7-ac2e-8f6cab61ae90.png%2Fpublic" alt="Type-driven security" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Security failures often begin at ordinary boundaries: a database record is returned directly from an API, an identifier from one domain is accepted in another, untrusted input is treated as valid, or a query is assembled as a string. Framework defaults, reviews, tests, and monitoring remain essential but a well-designed type system can make some of these mistakes harder to write and easier to spot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope matters:&lt;/strong&gt; types do not stop or eliminate OWASP vulnerabilities on their own. They cannot prove that an authenticated user is authorized, make hostile network input trustworthy, sanitize HTML, or replace parameterized database APIs. They are one defense-in-depth layer that can encode security-relevant intent in application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What types are good at
&lt;/h2&gt;

&lt;p&gt;Types are especially useful when a security property is about &lt;em&gt;which values may cross a boundary&lt;/em&gt;. They can distinguish public response data from persistence models, prevent accidental mixing of identifiers, require explicit construction of sensitive values, and make unsafe APIs inconvenient to call.&lt;/p&gt;

&lt;p&gt;That shifts some failures from production behavior to compiler feedback. It does not eliminate the need for runtime controls at trust boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use DTOs and controlled serialization to prevent accidental exposure
&lt;/h2&gt;

&lt;p&gt;Database models commonly contain fields that should never reach clients or logs. Returning them directly makes every caller responsible for remembering what to remove. Prefer an explicit response DTO and a single mapping function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;passwordHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mfaSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;internalNotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PublicUserDto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toPublicUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserRow&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PublicUserDto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// res.json(toPublicUser(user));&lt;/span&gt;
&lt;span class="c1"&gt;// Never serialize UserRow directly at an HTTP boundary.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allow-list is the important part: adding a sensitive database column does not automatically expose it. Apply the same idea to logging. Create log-specific DTOs or redaction helpers rather than passing arbitrary objects to &lt;code&gt;JSON.stringify&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LoginAudit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;LoginAudit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make identifier and authorization context explicit
&lt;/h2&gt;

&lt;p&gt;Two strings may have very different authority. A plain &lt;code&gt;string&lt;/code&gt; makes it easy to pass a tenant ID where an account ID is expected. Branded types can prevent accidental mixing inside TypeScript code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantBrand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unique&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userBrand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unique&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tenantBrand&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userBrand&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Query constrained by tenantId and userId.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authorization can be represented explicitly too, rather than relying on a vague boolean passed through several layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Viewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProjectEditor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;project-editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProjectAccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Viewer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;ProjectEditor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renameProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProjectEditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only a caller that established editor access can call this API.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This improves API design, but it is not authorization by itself. The server must still authenticate the request and verify the tenant, project membership, ownership, and policy against trusted data on every relevant request. Brands disappear at runtime, so external values must be validated before they are cast or constructed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep SQL parameterized types are not a SQL-injection fix
&lt;/h2&gt;

&lt;p&gt;Parameterized queries are the primary control against SQL injection. Types can help shape query inputs, but they do not make string interpolation safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Good: data remains a parameter, not SQL syntax.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT id, email FROM users WHERE tenant_id = $1 AND id = $2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Do not do this even if userId has a TypeScript type:&lt;/span&gt;
&lt;span class="c1"&gt;// db.query(`SELECT * FROM users WHERE id = '${userId}'`);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For dynamic identifiers such as sort columns, use a fixed allow-list and construct only from known literals. Parameter placeholders generally cannot substitute SQL identifiers or keywords.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTML requires a trusted/sanitized boundary
&lt;/h2&gt;

&lt;p&gt;Encoding and sanitization are runtime problems because HTML often originates outside the type checker. Treat raw user HTML and sanitized HTML as distinct concepts, but do not confuse a type assertion with sanitization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitizedHtmlBrand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unique&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SanitizedHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sanitizedHtmlBrand&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanitizeHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SanitizedHtml&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use a maintained sanitizer with an application-specific allow-list.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SanitizedHtml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderRichText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SanitizedHtml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use framework escaping by default. Only render HTML through a reviewed sanitizer, and account for URL policies, rich-text features, server-side rendering, and sanitizer updates. A &lt;code&gt;SanitizedHtml&lt;/code&gt; type is useful only if its constructor is tightly controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate runtime input before it becomes a domain type
&lt;/h2&gt;

&lt;p&gt;TypeScript types describe developer intent but are erased at runtime. JSON payloads, environment variables, queue messages, database rows, and third-party webhooks are all untrusted until validated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CreateProject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;private&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CreateProjectInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;CreateProject&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseCreateProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CreateProjectInput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;CreateProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation should happen at the edge, with clear error handling, size limits where appropriate, and authorization checks after identity and relevant resource state are known. Schema validation libraries complement static types; they do not replace business-rule validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs and failure modes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;More types and mappers:&lt;/strong&gt; DTOs, schemas, and branded values add code and maintenance.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Unsafe escape hatches:&lt;/strong&gt; broad &lt;code&gt;as&lt;/code&gt; casts, &lt;code&gt;any&lt;/code&gt;, and non-null assertions can bypass the guarantees. Restrict and review them.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;False confidence:&lt;/strong&gt; compile-time correctness says nothing about deployment configuration, access control policy, dependencies, or runtime input unless those are separately handled.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Boundary drift:&lt;/strong&gt; generated clients, ORMs, and serializers may bypass carefully designed domain types. Test the actual HTTP and database behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adoption checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; Identify high-risk boundaries: HTTP responses, logs, SQL access, webhooks, queues, and rich-text rendering.&lt;/li&gt;
&lt;li&gt; Introduce explicit public DTOs and allow-list serializers for sensitive entities.&lt;/li&gt;
&lt;li&gt; Use distinct ID and authorization-context types where mix-ups could cross tenant or resource boundaries.&lt;/li&gt;
&lt;li&gt; Require parameterized SQL APIs; allow-list dynamic identifiers.&lt;/li&gt;
&lt;li&gt; Validate all external data at runtime with schemas before creating domain values.&lt;/li&gt;
&lt;li&gt; Keep raw HTML separate from sanitized/trusted HTML, and use a maintained sanitizer.&lt;/li&gt;
&lt;li&gt; Back the design with authorization tests, integration tests, dependency updates, monitoring, and security review.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Strong types are most valuable when they make the secure path the natural path: explicit data transfer objects, constrained identifiers, narrow authority-bearing APIs, and visible trust boundaries. Used alongside runtime validation, parameterized queries, output encoding and sanitization, authorization enforcement, and operational controls, they can reduce the chance that common security mistakes reach production. They are not a substitute for those controls and they should never be presented as one.&lt;/p&gt;

</description>
      <category>functional</category>
      <category>security</category>
      <category>programming</category>
      <category>typesystems</category>
    </item>
    <item>
      <title>Architecture from Day One: The Practical Guide to Scalable Backend Systems</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 19 Jul 2026 05:20:11 +0000</pubDate>
      <link>https://dev.to/shubham399/architecture-from-day-one-the-practical-guide-to-scalable-backend-systems-4033</link>
      <guid>https://dev.to/shubham399/architecture-from-day-one-the-practical-guide-to-scalable-backend-systems-4033</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Ff98c60dd-4678-42e3-85ae-4b3df3eb8993%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Ff98c60dd-4678-42e3-85ae-4b3df3eb8993%2Fpublic" alt="Scalable backend systems architecture" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every backend engineer wants to build systems that scale. But scalability is not a feature to casually bolt onto version 2.0; it is an emergent property of deliberate architectural choices, operational discipline, and evidence from real workloads.&lt;/p&gt;

&lt;p&gt;After years of building and operating high-throughput systems across fintech and aviation, these are the foundational principles and production patterns that matter when moving from thousands of users to millions.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define “scalable” beyond the buzzwords
&lt;/h2&gt;

&lt;p&gt;Scaling is not simply handling more users. It is maintaining an agreed level of service as load grows: latency, correctness, availability, and cost all matter. A system that works for 1,000 users but collapses at 10,000 has a bottleneck to understand not merely “high load.”&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A practical rule:&lt;/strong&gt; do not judge scalability by raw requests per second alone. Define SLOs, then observe latency at the tail (especially p95 and p99), error rate, and resource saturation as load increases. A flat median can conceal a failing tail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run load tests that resemble production: realistic request mixes, payloads, connection behavior, data sizes, and downstream dependencies. Track queue depth, CPU, memory, database connections, disk and network saturation alongside user-facing SLOs. The useful question is: &lt;em&gt;at what load do the SLOs, error budget, or cost envelope stop being acceptable?&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Horizontal and vertical scaling: choose with evidence
&lt;/h3&gt;

&lt;p&gt;Vertical scaling is often the fastest, safest next step. A larger database instance, more memory for a cache, or a faster machine can be appropriate until availability, failure-domain, or cost limits make it unattractive. Horizontal scaling adds capacity and resilience, but also coordination, deployment, and consistency complexity.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stateless services:&lt;/strong&gt; scale horizontally when demand and redundancy justify it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stateful services:&lt;/strong&gt; first optimize queries, indexes, schema, and instance sizing; use replicas, partitioning, or sharding when measurements show they are needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mixed workloads:&lt;/strong&gt; separate stateful and stateless responsibilities so each can be tuned and scaled independently.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sharding is not a default milestone. It raises operational and application complexity routing, rebalancing, cross-shard queries, and recovery. Introduce it only when observed data volume, write throughput, storage, or availability requirements exceed what simpler approaches can meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Stateless design enables elastic application capacity
&lt;/h2&gt;

&lt;p&gt;Keeping request-specific state out of application memory makes ordinary HTTP request handling easier to distribute across instances. Sessions, shared rate-limit counters, and durable workflow state should live in purpose-built external stores rather than a process-local map.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: process-local session state is lost on restart and is not shared.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Better: use a shared store with expiry and appropriate availability controls.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`session:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Statelessness does &lt;strong&gt;not&lt;/strong&gt; mean every instance can be terminated without coordination. WebSockets, server-sent events, streaming responses, in-flight requests, local uploads, and long-running jobs may still be attached to an instance. Use readiness checks and graceful draining: stop accepting new work, allow bounded in-flight work to finish, notify or reconnect long-lived clients when appropriate, and enforce a termination deadline. Sticky routing may still be useful for connection affinity or performance, even if it is not required for ordinary shared-session HTTP traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Multi-layer caching without correctness surprises
&lt;/h2&gt;

&lt;p&gt;Caching is high leverage for read-heavy workloads, but its common risks are stale reads, inconsistent invalidation, cache-key mistakes, eviction behavior, and thundering herds not an automatic guarantee of data corruption or split brain. Treat the backing datastore as the source of truth unless you have explicitly designed a stronger consistency model.&lt;/p&gt;

&lt;p&gt;Cache layer&lt;/p&gt;

&lt;p&gt;Typical targets&lt;/p&gt;

&lt;p&gt;Useful characteristics&lt;/p&gt;

&lt;p&gt;Edge / CDN&lt;/p&gt;

&lt;p&gt;Public, cacheable assets and responses&lt;/p&gt;

&lt;p&gt;Low latency near users; deliberate cache-control and purge strategy&lt;/p&gt;

&lt;p&gt;Application / distributed cache&lt;/p&gt;

&lt;p&gt;Derived objects, sessions, rate limits&lt;/p&gt;

&lt;p&gt;Shared across instances; explicit TTLs, keys, and invalidation&lt;/p&gt;

&lt;p&gt;Database buffer / replicas&lt;/p&gt;

&lt;p&gt;Frequently read database pages and read traffic&lt;/p&gt;

&lt;p&gt;Helps throughput, but replicas can have replication lag&lt;/p&gt;

&lt;p&gt;Choose a pattern deliberately: cache-aside is simple for many reads; write-through can reduce stale-cache windows; write-behind trades simplicity for durability and recovery concerns. Version cache keys when schemas change, invalidate or update entries on writes, and set bounded TTLs even when invalidation exists.&lt;/p&gt;

&lt;p&gt;Protect the origin from a cache stampede. Coalesce concurrent misses with request locking or single-flight, refresh hot entries ahead of expiry where suitable, and add jitter to TTLs so many keys do not expire together. Monitor hit rate, miss rate, eviction, keyspace growth, memory pressure, origin fall-through, refresh failures, and stale-serving behavior. Test failure modes: cache unavailable, invalidation delayed, and an expired hot key under peak traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Data scaling and the CAP theorem
&lt;/h2&gt;

&lt;p&gt;Replication improves read capacity and resilience, but it introduces lag and failover trade-offs. Route reads only where the consistency required by the operation is available; a user who has just written data may need read-your-writes behavior rather than an asynchronous replica.&lt;/p&gt;

&lt;p&gt;CAP is specifically about what a distributed system does &lt;strong&gt;during a network partition&lt;/strong&gt;. When replicas cannot communicate, a system cannot simultaneously guarantee both a single consistent view of data and availability of every request. The design chooses its behavior per operation: reject or block some requests to preserve consistency, or serve potentially stale/divergent data and reconcile later. Outside a partition, latency, quorum configuration, and implementation choices still determine practical behavior.&lt;/p&gt;

&lt;p&gt;Use evidence before introducing partitions or shards: sustained write bottlenecks, storage limits, noisy-neighbor isolation, geographic requirements, or demonstrated availability needs. Define a shard key that spreads traffic, avoid cross-shard transactions where possible, and plan rebalancing, backups, and repair before the first shard is created.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Control overload at every boundary
&lt;/h2&gt;

&lt;p&gt;Load balancers distribute traffic; they do not create infinite capacity. Set connection and concurrency limits at services and dependencies, and propagate deadlines so doomed work does not continue consuming resources. Timeouts should be explicit and shorter than the caller’s remaining deadline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backpressure and load shedding:&lt;/strong&gt; bound queues, reject low-priority work early, and return clear overload responses instead of allowing unbounded latency and memory growth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retries:&lt;/strong&gt; retry only operations that are safe or idempotent; use exponential backoff with jitter, a maximum attempt count, and a retry budget so an incident does not become a retry storm.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Circuit breakers:&lt;/strong&gt; temporarily stop calls to a demonstrably unhealthy dependency, fail fast or use a defined fallback, and probe recovery carefully.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bulkheads:&lt;/strong&gt; isolate thread pools, connection pools, queues, and tenant limits so one slow dependency or customer does not exhaust the whole service.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instrument saturation and queueing, not only successful request rate. Alert on SLO burn, p95/p99 regressions, error rate, exhausted pools, queue age, and dependency health.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Event-driven architecture needs delivery discipline
&lt;/h2&gt;

&lt;p&gt;Event-driven architecture (EDA) can decouple producers from consumers and smooth bursty work, but a broker does not remove distributed-systems failure modes. Most practical consumers operate with at-least-once delivery, so duplicates are normal rather than exceptional.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Make handlers &lt;strong&gt;idempotent&lt;/strong&gt; using stable event IDs, deduplication records, or idempotent writes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry transient failures with bounded exponential backoff and jitter; send poison messages to a monitored dead-letter queue (DLQ) with a replay process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Document ordering scope. Ordering may exist only within a partition/key, and retries can change the apparent order.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use the transactional outbox pattern when a database write and event publication must not diverge. Persist the intent with the business transaction, then reliably relay it to the broker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Version event schemas and preserve compatibility during producer and consumer rollouts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure lag, consumer throughput, retry counts, DLQ volume, duplicate rate, and end-to-end processing latency. These metrics turn “asynchronous” into an observable service commitment.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Build the feedback loop
&lt;/h2&gt;

&lt;p&gt;Capacity planning is a continuous loop: establish SLOs, instrument real workloads, test failure and load scenarios, remove the measured bottleneck, and repeat. Prefer the simplest architecture that meets current reliability and growth needs, while leaving clean seams for the next proven constraint.&lt;/p&gt;

&lt;p&gt;Scalable systems are not the ones with the most components. They are the ones that make trade-offs explicit, degrade predictably under stress, and give operators enough observability to act before customers notice.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>performance</category>
    </item>
    <item>
      <title>How I Built a Personal AI Assistant That Lives in Telegram</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 19 Jul 2026 05:15:49 +0000</pubDate>
      <link>https://dev.to/shubham399/how-i-built-a-personal-ai-assistant-that-lives-in-telegram-1j8o</link>
      <guid>https://dev.to/shubham399/how-i-built-a-personal-ai-assistant-that-lives-in-telegram-1j8o</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fd751c8d2-02d0-4f0c-8c7e-a0eb64532ab0%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fd751c8d2-02d0-4f0c-8c7e-a0eb64532ab0%2Fpublic" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wanted a personal AI assistant that lived where I already communicate: Telegram. Not another dashboard to remember, not a browser tab that disappears into the pile, and not a demo that can write clever text but cannot actually help me do things.&lt;/p&gt;

&lt;p&gt;The result is a Telegram bot that can answer questions, remember useful context, schedule reminders, retrieve information, and use connected services through tightly controlled tools. It is intentionally practical rather than magical. The interesting work was not making a model produce text; it was building the systems around it so that tool use, scheduling, failures, and external side effects behave predictably.&lt;/p&gt;

&lt;p&gt;This post explains the architecture, the trade-offs I made, and the safeguards that make a personal assistant useful without turning it into an unattended automation machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Optimized For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Telegram-first interaction:&lt;/strong&gt; send a message, receive a useful response, and avoid a separate product surface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tool use with boundaries:&lt;/strong&gt; it can retrieve data and invoke integrations, but it cannot freely perform side effects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Durable personal state:&lt;/strong&gt; reminders, notes, job history, and operational records must survive restarts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simple operations:&lt;/strong&gt; one deployable service, observable logs, backups, and understandable failure modes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Honest scaling limits:&lt;/strong&gt; start with SQLite and one active bot consumer; change the architecture only when the workload requires it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not optimize for a fully autonomous agent. For a personal assistant, reliability and control are more valuable than letting a model take unlimited actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;At a high level, the system has five layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Telegram ingress:&lt;/strong&gt; Telegraf receives updates and normalizes messages into an application request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conversation orchestration:&lt;/strong&gt; the application loads relevant context, calls OpenAI, and runs a bounded tool-execution loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tool layer:&lt;/strong&gt; local capabilities such as notes, reminders, weather, and database reads sit behind explicit schemas and policies. Connected third-party services are accessed through Composio.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Durable state:&lt;/strong&gt; SQLite stores sessions, scheduled jobs, execution attempts, idempotency keys, and operational data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Background worker:&lt;/strong&gt; a scheduler claims due jobs, executes them, records the result, and retries safely where appropriate.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Telegram
   │
   ▼
Telegraf handler ──► auth + rate limits ──► assistant orchestrator
                                              │
                         ┌────────────────────┼────────────────────┐
                         ▼                    ▼                    ▼
                     OpenAI API          local tools           Composio
                         │                    │                    │
                         └──────────────► SQLite ◄─────────────────┘
                                               ▲
                                               │
                                        scheduler worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application is deliberately not a collection of unconstrained agents talking to each other. A single orchestrator owns the request lifecycle. That makes it easier to trace what happened, apply policy consistently, and keep failures from becoming confusing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bun, Telegraf, OpenAI, Composio, and SQLite
&lt;/h2&gt;

&lt;p&gt;I chose &lt;strong&gt;Bun&lt;/strong&gt; because it gives me a fast TypeScript runtime, package management, and a straightforward deployment target. It keeps the service compact without requiring a complicated build pipeline for a small application. Bun is not the reason the assistant is reliable, though; explicit application boundaries and durable state are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telegraf&lt;/strong&gt; is a mature, ergonomic Telegram framework. It handles the Telegram update format well while leaving routing, middleware, and error handling under my control. The bot layer should be boring: validate the sender, acknowledge the message lifecycle, and hand work to the application layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI&lt;/strong&gt; provides the language model and structured tool-calling interface. Tool definitions help the model select an operation and produce arguments in an expected shape. They do not replace runtime validation. The model can still select an inappropriate tool, provide malformed data, or request an action the current user should not be allowed to take.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composio&lt;/strong&gt; is useful for OAuth-backed integrations. Instead of implementing every third-party OAuth flow, token lifecycle, and API wrapper myself, I can use a consistent connection layer for supported external services. That convenience does not eliminate security work: every integration still needs an allowlist, narrow scopes, and separate treatment for read operations versus side effects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite&lt;/strong&gt; is the right database while the assistant is a single-user or low-volume system. It is portable, inexpensive to operate, and excellent for transactional local state. I use it for data that must be durable: reminders, job execution records, sessions, and idempotency keys. It is not a distributed queue, and it is not the long-term answer for multiple independently writing application instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling Instead of Webhooks
&lt;/h2&gt;

&lt;p&gt;I use Telegram long polling rather than webhooks. For a personal deployment, polling avoids exposing a public HTTPS endpoint, certificate management, reverse-proxy setup, and webhook routing. The process asks Telegram for updates, processes them, and advances through the update stream.&lt;/p&gt;

&lt;p&gt;The important caveat is that polling needs exactly one active consumer for a bot token. Running two polling instances at once can create conflicts and unpredictable update handling. If I deploy a replacement instance, I make sure the previous consumer is stopped before the new one begins polling.&lt;/p&gt;

&lt;p&gt;Offset handling matters too. Telegram updates have monotonically increasing identifiers, and the consumer must advance its offset only after it has safely recorded or processed an update. In practice, I also keep a durable update or message idempotency record. That protects against duplicate handling after a process crash, a network timeout, or a restart near the boundary between receiving and committing an update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const seen = db.query(
  "SELECT 1 FROM processed_updates WHERE update_id = ?"
);

async function handleUpdate(update: TelegramUpdate) {
  if (seen.get(update.update_id)) return;

  db.run("BEGIN IMMEDIATE");
  try {
    db.run(
      "INSERT INTO processed_updates (update_id, processed_at) VALUES (?, ?)",
      [update.update_id, new Date().toISOString()]
    );
    db.run("COMMIT");
  } catch (error) {
    db.run("ROLLBACK");
    throw error;
  }

  await processMessage(update);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact transaction design depends on what is being processed, but the principle is consistent: duplicate delivery is normal in distributed systems, so handlers should be safe to run more than once.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Message to Tool Call to Reply
&lt;/h2&gt;

&lt;p&gt;When a Telegram message arrives, the bot does not immediately hand raw text to a model and execute whatever comes back. The request follows a controlled pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Verify that the chat and user are permitted to use the assistant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apply per-user and global rate limits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Load the minimum relevant conversation context and persistent memory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Send the model a system policy, the user message, and a small allowlisted tool catalog.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Validate every requested tool call against a runtime schema and authorization policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Execute approved tools, append structured results, and continue the model loop within a strict step limit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Persist useful state and send the final answer back through Telegram.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model loop is intentionally bounded. A tool-capable model can ask for another tool result after receiving the previous one, so a useful assistant needs multiple steps. But it also needs a ceiling to prevent accidental loops, excessive API cost, or an unexpected chain of actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const MAX_TOOL_STEPS = 5;

for (let step = 0; step &amp;lt; MAX_TOOL_STEPS; step++) {
  const response = await openai.responses.create({
    model: MODEL,
    input,
    tools: allowedToolsFor(user),
  });

  const calls = extractToolCalls(response);
  if (calls.length === 0) {
    return extractText(response);
  }

  for (const call of calls) {
    const result = await runApprovedTool({
      userId: user.id,
      chatId: chat.id,
      call,
    });

    input.push(toolResultMessage(call, result));
  }
}

throw new Error("Tool loop exceeded its configured limit");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also set timeouts at the boundaries: Telegram delivery, model requests, database operations, and external integrations. One slow provider should not hold a message handler forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Safe Tool Policy
&lt;/h2&gt;

&lt;p&gt;Tool calling is where an assistant becomes useful and where it can become unsafe. My policy is based on capability rather than prompt wording.&lt;/p&gt;

&lt;p&gt;First, tools are &lt;strong&gt;allowlisted&lt;/strong&gt;. The model sees only tools appropriate for the current user and context. A read-only stock quote tool, for example, is fundamentally different from a tool that sends an email or creates a calendar event. I do not expose administrative or infrastructure operations to a general chat flow just because the model could describe them.&lt;/p&gt;

&lt;p&gt;Second, every tool has &lt;strong&gt;runtime argument validation&lt;/strong&gt;. TypeScript types are useful during development, but they disappear at runtime. Tool arguments from a model or an external API are untrusted input. I validate them with a schema library or explicit checks before calling application code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const createReminderSchema = z.object({
  task: z.string().min(1).max(500),
  time: z.string().regex(/^\d{2}:\d{2}$/),
  scheduleType: z.enum(["once", "daily", "weekdays", "weekly"]),
  dayOfWeek: z.string().optional(),
});

function validateReminder(args: unknown) {
  return createReminderSchema.parse(args);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, external side effects require &lt;strong&gt;explicit confirmation&lt;/strong&gt;. If the assistant is about to send a message, create an event, modify a document, or perform another consequential action, it prepares a preview and asks the user to confirm. A confirmation is tied to the intended action, expires quickly, and is consumed once. The system should not interpret “yes” from an unrelated later conversation as approval to send something.&lt;/p&gt;

&lt;p&gt;Fourth, side-effecting operations receive an &lt;strong&gt;idempotency key&lt;/strong&gt;. A network failure after a provider accepts a request is ambiguous: retrying without a key can create duplicate events, emails, or tasks. Where a provider supports idempotency, I pass a stable key. Where it does not, I persist an operation record and use provider-specific lookup or reconciliation where possible.&lt;/p&gt;

&lt;p&gt;Finally, OAuth connections are &lt;strong&gt;scoped and isolated&lt;/strong&gt;. I request the narrowest permissions needed, avoid broad account access by default, store connection references rather than casually exposing raw tokens, and make disconnecting an integration straightforward. Secrets belong in the deployment environment or a secret manager, never in source control, logs, prompts, or tool output.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite, WAL, and Scheduling Correctness
&lt;/h2&gt;

&lt;p&gt;I run SQLite in write-ahead logging mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WAL improves concurrency for this workload because readers can continue while a writer is committing. It does &lt;em&gt;not&lt;/em&gt; turn SQLite into a multi-writer database. There is still one writer at a time, so transactions should be short, indexes should support the scheduler’s queries, and write-heavy background work should not be mixed carelessly with long interactive transactions.&lt;/p&gt;

&lt;p&gt;The scheduler stores jobs in SQLite rather than trusting in-memory timers. A worker periodically finds due jobs, atomically claims one, runs it, and records the outcome. The claim prevents two worker loops from executing the same job simultaneously in the same database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE scheduled_jobs
SET status = 'running',
    locked_at = :now,
    lock_token = :token
WHERE id = (
  SELECT id
  FROM scheduled_jobs
  WHERE status = 'pending'
    AND run_at &amp;lt;= :now
  ORDER BY run_at
  LIMIT 1
)
AND status = 'pending';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A worker is not a durable queue merely because it runs in a loop. Durability comes from the database records: pending jobs, attempts, lock timestamps, completion state, and idempotency keys. If the process dies after claiming a job, recovery logic detects stale locks and returns eligible work to the pending state. If the process dies after an external side effect but before recording completion, idempotency and reconciliation logic determine whether it is safe to retry.&lt;/p&gt;

&lt;p&gt;For recurring jobs, I calculate the next run after a successful execution and store times consistently, typically in UTC with the user’s timezone retained for display and recurrence rules. Timezones and daylight-saving transitions deserve dedicated tests; “every day at 9” is more complicated than adding 24 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling, Rate Limits, and Operations
&lt;/h2&gt;

&lt;p&gt;Every integration can fail. Telegram can time out, an OAuth token can be revoked, OpenAI can rate-limit a request, and a third-party API can return malformed data. The assistant should explain failures plainly without leaking secrets or internal stack traces.&lt;/p&gt;

&lt;p&gt;I categorize errors into retryable and non-retryable classes. Network timeouts, temporary 429 responses, and many 5xx errors can be retried with exponential backoff and jitter. Invalid arguments, revoked permissions, and user-denied confirmations should not be blindly retried. Retries have caps, deadlines, and structured logs so a bad provider does not create an infinite background loop.&lt;/p&gt;

&lt;p&gt;Rate limits exist at multiple layers: Telegram message handling, model calls, tool calls, and external APIs. For interactive chat, a per-user token bucket or short rolling window is usually sufficient. I also limit tool-loop depth, tool-call count, payload size, and concurrent outbound requests. These controls protect both cost and availability.&lt;/p&gt;

&lt;p&gt;Deployment is intentionally simple: one application instance, persistent storage mounted outside ephemeral container layers, environment-based configuration, and a process supervisor or platform health checks. Before each deploy, I run migrations in a controlled step and ensure the prior polling consumer has stopped. Health checks verify that the process is alive; readiness checks should also verify that configuration and the database are usable.&lt;/p&gt;

&lt;p&gt;Backups are not optional because SQLite is the system of record. I take regular backups from a consistent SQLite snapshot, retain multiple recovery points, encrypt backups where appropriate, and periodically test restoration. A backup that has never been restored is only a theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests, Observability, and Scaling Boundaries
&lt;/h2&gt;

&lt;p&gt;The highest-value tests are not model snapshot tests. They cover authorization decisions, runtime validation, confirmation expiry, idempotency, scheduler claims, stale-lock recovery, timezone behavior, and duplicate Telegram updates. I use mocked provider clients for deterministic unit tests, then run a small number of integration tests against isolated credentials or test resources.&lt;/p&gt;

&lt;p&gt;For observability, each incoming Telegram update receives a correlation id. Logs include the update id, user or chat identifier where safe, request duration, model request id when available, tool name, retry count, and job id. I record metrics for error rates, latency, tool failures, queue age, worker recovery, and rate-limit rejections. I log metadata, not secrets or private message content by default.&lt;/p&gt;

&lt;p&gt;This architecture has clear scaling boundaries. SQLite with one active polling process is excellent for a personal assistant and modest traffic. It becomes a constraint when multiple application instances need concurrent writes, background work grows significantly, or webhook-based horizontal ingress becomes necessary. At that point, I would move durable state to a server database such as Postgres and use a real queue for independently scalable workers. I would not pretend that adding more containers around one SQLite file solves distributed coordination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create a Telegram bot and restrict initial access to known user or chat ids.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start with Telegraf long polling and ensure only one consumer runs at a time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build a small OpenAI orchestration loop with a maximum number of tool steps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Expose only an explicit, per-user allowlist of tools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Validate every tool argument at runtime before execution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Split read-only tools from side-effecting tools; require preview and confirmation for the latter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use scoped OAuth connections and keep credentials out of code, prompts, and logs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Persist jobs, attempts, locks, and idempotency keys in SQLite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enable WAL, keep write transactions short, and plan around SQLite’s single-writer model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement stale-lock recovery and bounded retry behavior for workers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add structured logs, metrics, alerts, and restoration-tested backups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test duplicates, retries, revocations, crashes, and timezones before relying on automation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Repository and Implementation Notes
&lt;/h2&gt;

&lt;p&gt;The implementation evolves, but the core idea remains stable: keep the chat interface simple and put the engineering effort into policy, persistence, and recoverability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/shubhkumar/ai-agent" rel="noopener noreferrer"&gt;View the project source on GitHub&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;A useful personal AI assistant is less about giving a model unlimited access and more about designing reliable boundaries around it. Telegram provides the interface, OpenAI provides reasoning and language, Composio can provide controlled access to connected services, and SQLite provides a durable foundation for a small deployment.&lt;/p&gt;

&lt;p&gt;The hard parts are familiar engineering problems: authorization, input validation, duplicate delivery, idempotency, retries, recovery, backups, and observability. Solving those deliberately turns an impressive chatbot demo into an assistant I can trust to use every day.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>What's All Am I Hosting? Full Infrastructure Breakdown</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sat, 04 Jul 2026 18:52:41 +0000</pubDate>
      <link>https://dev.to/shubham399/whats-all-am-i-hosting-full-infrastructure-breakdown-53b8</link>
      <guid>https://dev.to/shubham399/whats-all-am-i-hosting-full-infrastructure-breakdown-53b8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Ff9ea98b1-39b2-4983-85cd-a1d67470ae45.png%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Ff9ea98b1-39b2-4983-85cd-a1d67470ae45.png%2Fpublic" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every few months, someone asks me how I run my entire online presence for basically nothing. The short answer is: I don't pay for what I can get for free.&lt;/p&gt;

&lt;p&gt;The long answer is this post.&lt;/p&gt;

&lt;p&gt;My entire infrastructure   this site, APIs, email, monitoring, URL shortener, dev tools   runs on free-tier cloud services. Total cost: &lt;strong&gt;$0/month&lt;/strong&gt;. That's less than a single AWS load balancer costs for an hour.&lt;/p&gt;

&lt;p&gt;Here's exactly how it works, why I chose each piece, and what I'd do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Philosophy: Free Tier First
&lt;/h2&gt;

&lt;p&gt;I have a simple rule: if a service has a generous free tier that covers my use case, I use it. If I outgrow it, I'll pay   but most of us never outgrow free tiers for personal projects.&lt;/p&gt;

&lt;p&gt;The second rule: own the critical paths. DNS is the backbone, so it's on Cloudflare (free, but best-in-class). The main site is on Vercel (free, seamless Next.js deployment). Everything else   databases, email, APIs, monitoring   is a managed service that solves exactly one problem well.&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS: Cloudflare (The Glue That Holds It All Together)
&lt;/h2&gt;

&lt;p&gt;Everything starts with DNS. Cloudflare runs &lt;strong&gt;chan.ns.cloudflare.com&lt;/strong&gt; and &lt;strong&gt;dave.ns.cloudflare.com&lt;/strong&gt; as my authoritative nameservers. Every subdomain in this post is a DNS record that Cloudflare serves for free.&lt;/p&gt;

&lt;p&gt;I use Cloudflare as a pure DNS provider   no CDN proxying on most records except a few redirects (cal, link, mail). The proxied records hide my origin IP and give me free SSL termination, but I keep most records direct because I want full control over the traffic path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Cloudflare over Route53 or self-hosted?&lt;/strong&gt; Cloudflare's free plan includes unlimited DNS queries, DNSSEC, easy API access, and their dashboard is fast. Route53 charges per query. Self-hosting DNS is unnecessary complexity. Free + best-in-class = no contest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Main Site: Vercel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;shubhkumar.in&lt;/strong&gt; is hosted on Vercel's free tier. The apex domain uses an A record to 216.198.79.1 (Vercel's anycast IP for apex domains   you can't use a CNAME at the root).&lt;/p&gt;

&lt;p&gt;Vercel's free tier includes 100GB bandwidth, 6000 build minutes, automatic SSL, and edge network distribution. For a Next.js site with ISR, this is more than enough. The site loads fast everywhere because Vercel serves it from their edge network.&lt;/p&gt;

&lt;p&gt;The www subdomain CNAMEs to the root   standard practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Vercel-Hosted Sites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;cv.shubhkumar.in&lt;/strong&gt;   Resume site. Next.js, deploys from GitHub.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;news.shubhkumar.in&lt;/strong&gt;   Another Next.js site.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hosted Applications: The Free Tier Dream Team
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Render   API Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;api.shubhkumar.in&lt;/strong&gt; runs on Render's free tier. It handles contact forms, webhooks, and server-side endpoints the static site can't handle. Render gives 750 hours/month   plenty for a low-traffic personal API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Render over Railway or Fly.io?&lt;/strong&gt; Render has the most generous free tier for this use case. The deploy experience is smooth (Git push → deploy), and SSL is automatic.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Pages   Lightweight Pages
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;connect.shubhkumar.in&lt;/strong&gt; is a simple social link aggregator   Linktree-style but self-hosted. Single HTML page, zero cost, instant deploy from a GitHub repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure: The Stuff That Runs in the Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tailscale   Private Network Bridge
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;tail.shubhkumar.in&lt;/strong&gt; points to my Tailscale node at &lt;strong&gt;100.115.133.19&lt;/strong&gt;. Tailscale creates a WireGuard mesh across all my devices   laptop, home server, cloud VMs. The DNS record lets me reach my home lab from anywhere using a proper subdomain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real magic:&lt;/strong&gt; Tailscale handles NAT traversal automatically. My home server is behind CGNAT (common with Indian ISPs), and Tailscale punches through without any port forwarding. Free tier: up to 100 devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Databases   Managed, Never Self-Hosted
&lt;/h3&gt;

&lt;p&gt;I use managed databases exclusively. &lt;strong&gt;Supabase&lt;/strong&gt; (Postgres) for anything that needs relational queries and real-time subscriptions. &lt;strong&gt;MongoDB Atlas&lt;/strong&gt; for document storage when the schema is fluid. Both on free tiers or their cheapest paid plans   whichever covers the workload.&lt;/p&gt;

&lt;p&gt;Self-hosting a database is the fastest way to turn a weekend into an ops nightmare. Backups, replication, patches, disk space   all someone else's problem. The managed premium is worth every rupee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fly.io   Lightweight Apps
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;track.shubhkumar.in&lt;/strong&gt; hosts WakaAPI (self-hosted WakaTime stats) on Fly.io. Free allowance covers the tiny resource usage   3 shared-CPU VMs with 256MB RAM, 3GB storage, 160GB outbound.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cal.com   Scheduling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;cal.shubhkumar.in&lt;/strong&gt;   Cloudflare-proxied redirect to Cal.com. Self-hosting a calendar scheduler isn't worth my weekend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Uptime   Monitoring
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;status.shubhkumar.in&lt;/strong&gt;   Free tier. Monitors all endpoints, notifies on Slack. 10 monitors with 3-minute checks and a public status page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools and Redirects
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dub.co   URL Shortener
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;go.shubhkumar.in&lt;/strong&gt; runs on Dub.co's free tier. Short, memorable links   &lt;em&gt;go.shubhkumar.in/github&lt;/em&gt;, etc. Open-source, great API, free tier includes custom domains and basic analytics.&lt;/p&gt;

&lt;p&gt;Used to run YOURLS on a VPS. Moving to Dub.co saved maintenance and gave better analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple Redirects
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;link.shubhkumar.in&lt;/strong&gt; and &lt;strong&gt;mail.shubhkumar.in&lt;/strong&gt; are Cloudflare-proxied redirects using 192.0.2.1 (Cloudflare's placeholder IP). No server needed   Cloudflare page rules handle the redirects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email Infrastructure: The Hardest Part
&lt;/h2&gt;

&lt;p&gt;Email is the hardest thing on a personal domain. I run three services for different purposes because each solves a specific problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zoho Mail   Primary
&lt;/h3&gt;

&lt;p&gt;Primary email on Zoho's free plan   5 mailboxes with 5GB each, custom domain, IMAP/SMTP, calendar. The last remaining free tier for professional email on a custom domain after Outlook killed their free custom domain offering and Google Workspace charges $6/user/month.&lt;/p&gt;

&lt;p&gt;MX: mx.zoho.com (priority 10), mx2/3 as fallbacks. DMARC set to &lt;strong&gt;reject&lt;/strong&gt; with Cloudflare reporting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Zoho's spam filtering is decent but not Gmail-level. For free, acceptable.&lt;/p&gt;

&lt;h3&gt;
  
  
  SimpleLogin   Email Aliases
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;simple.shubhkumar.in&lt;/strong&gt;   Creates aliases that forward to my primary inbox. If a service sells my email, I delete the alias. Free tier: 15 aliases, PGP encryption, open-source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resend   Transactional + Broadcast Emails
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;Resend&lt;/strong&gt; for all outgoing emails   transactional notifications, broadcast newsletters, and contact form submissions. It handles everything through a single clean API with good deliverability out of the box.&lt;/p&gt;

&lt;p&gt;Resend's free tier includes 100 emails/day, which covers my low-volume needs. DKIM and SPF configured through forms.shubhkumar.in for proper authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Verification Records
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DMARC (reject)&lt;/strong&gt;   Strictest policy. Reports to Cloudflare's DMARC reporting. Caught email spoofing attempts at least twice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;BIMI&lt;/strong&gt;   Shows my logo next to authenticated emails. Mostly vanity but looks professional.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keybase&lt;/strong&gt;   Domain ownership proof.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Google Search Console&lt;/strong&gt;   Site ownership for search analytics.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Don't Use (And Why)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Kubernetes.&lt;/strong&gt; Everything fits in docker-compose or managed platforms. K8s is operational overkill for one person.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No AWS.&lt;/strong&gt; Pricing model punishes hobbyists. One misconfigured resource = surprise bill. I use SES for email only when necessary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No self-hosted CI.&lt;/strong&gt; GitHub Actions is free for public repos.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Databases:&lt;/strong&gt; Managed   Supabase (Postgres), MongoDB Atlas, etc. Free tiers + cheapest plans cover everything. Self-hosting a database is unnecessary ops overhead.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost Breakdown
&lt;/h2&gt;

&lt;p&gt;Service&lt;/p&gt;

&lt;p&gt;Cost&lt;/p&gt;

&lt;p&gt;What It Runs&lt;/p&gt;

&lt;p&gt;Vercel&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;Main site (Next.js) + CV + News&lt;/p&gt;

&lt;p&gt;Cloudflare DNS&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;All DNS records, proxied redirects&lt;/p&gt;

&lt;p&gt;Render&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;API server&lt;/p&gt;

&lt;p&gt;GitHub Pages&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;Link aggregator&lt;/p&gt;

&lt;p&gt;Fly.io&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;WakaAPI instance&lt;/p&gt;

&lt;p&gt;Better Uptime&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;10 monitors, status page&lt;/p&gt;

&lt;p&gt;Dub.co&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;URL shortener&lt;/p&gt;

&lt;p&gt;Zoho Mail&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;Primary email, custom domain&lt;/p&gt;

&lt;p&gt;SimpleLogin&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;15 email aliases&lt;/p&gt;

&lt;p&gt;Resend&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;Transactional + broadcast emails&lt;/p&gt;

&lt;p&gt;Tailscale&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;Mesh VPN, 100 devices&lt;/p&gt;

&lt;p&gt;Supabase&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;Managed Postgres (free tier)&lt;/p&gt;

&lt;p&gt;MongoDB Atlas&lt;/p&gt;

&lt;p&gt;$0&lt;/p&gt;

&lt;p&gt;Managed MongoDB (free tier)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$0/mo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;~15 services, one domain, full infra&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You don't need to self-host everything.&lt;/strong&gt; Used to run my own email server, Git server, CI   huge time sink for zero benefit. Managed services let you focus on what matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DNS TTL matters.&lt;/strong&gt; Low TTL (1–300s) on frequently changed records. High TTL (86400) on stable records for faster lookups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Email deliverability is a second job.&lt;/strong&gt; DKIM + SPF + DMARC + BIMI + reverse DNS + feedback loops takes a full day to set up. Get it right once, don't touch it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Free tiers are designed to hook you.&lt;/strong&gt; That's fine as long as you understand the migration cost before you're locked in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitoring is not optional.&lt;/strong&gt; Better Uptime caught three outages I wouldn't have noticed until someone emailed me.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Reality
&lt;/h2&gt;

&lt;p&gt;Running your own infrastructure is a trade-off   setup time and occasional debugging for complete control and zero ongoing cost. For me, it's worth it.&lt;/p&gt;

&lt;p&gt;But I also know when to stop. I don't self-host email. I don't run a Docker registry. I don't build custom dashboards. The services I chose handle those well enough that my time is better spent building on top of them.&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building My Personal Website From Scratch: Tech Stack, Architecture, and Lessons Learned</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sat, 27 Jun 2026 19:12:59 +0000</pubDate>
      <link>https://dev.to/shubham399/building-my-personal-website-from-scratch-tech-stack-architecture-and-lessons-learned-1h04</link>
      <guid>https://dev.to/shubham399/building-my-personal-website-from-scratch-tech-stack-architecture-and-lessons-learned-1h04</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fd4c3d0f2-ed81-44ed-a086-41ebb083e8df%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fd4c3d0f2-ed81-44ed-a086-41ebb083e8df%2Fpublic" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A personal website is more than just a portfolio it's a playground for experimenting with architecture, performance, and production-ready engineering.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Ask ten software engineers what their personal website is for, and you'll probably hear the same answer: &lt;em&gt;"It's my portfolio."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While that's true, I wanted mine to be something more.&lt;/p&gt;

&lt;p&gt;I wanted &lt;a href="http://shubhkumar.in" rel="noopener noreferrer"&gt;&lt;strong&gt;shubhkumar.in&lt;/strong&gt;&lt;/a&gt; to be a platform that could grow with me a place to showcase projects, host my CV, publish technical blogs, and experiment with ideas before applying them in production systems.&lt;/p&gt;

&lt;p&gt;Rather than using a static template or website builder, I decided to build everything from scratch. My goal wasn't to use the most technologies possible; it was to create a clean architecture that was fast, maintainable, and easy to extend.&lt;/p&gt;

&lt;p&gt;Today, the website consists of two main parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;Next.js&lt;/strong&gt; frontend deployed on &lt;strong&gt;Vercel&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;Node.js + Express&lt;/strong&gt; backend deployed on &lt;strong&gt;Render&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Behind the scenes, &lt;strong&gt;MongoDB&lt;/strong&gt; stores dynamic content, while &lt;strong&gt;Redis&lt;/strong&gt; speeds up API responses through caching.&lt;/p&gt;

&lt;p&gt;It sounds like a fairly standard stack and in many ways, it is. But the interesting part wasn't choosing the technologies. It was designing how they work together.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Build It From Scratch?
&lt;/h1&gt;

&lt;p&gt;There are countless templates and portfolio generators available today. They look great, take minutes to deploy, and require almost no maintenance.&lt;/p&gt;

&lt;p&gt;So why spend time building everything yourself?&lt;/p&gt;

&lt;p&gt;For me, the answer was simple.&lt;/p&gt;

&lt;p&gt;I wanted complete control.&lt;/p&gt;

&lt;p&gt;Not just over the design, but over the architecture.&lt;/p&gt;

&lt;p&gt;I wanted a backend that wasn't tightly coupled to a frontend. I wanted my content to live in one place instead of being duplicated across pages. Most importantly, I wanted a project that reflected how I build software professionally.&lt;/p&gt;

&lt;p&gt;Every new feature became an opportunity to solve a real engineering problem instead of simply adding another section to a webpage.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Tech Stack
&lt;/h1&gt;

&lt;p&gt;I deliberately kept the stack simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hosted on Vercel&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next.js gives me everything I need for a modern website:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Server Components&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Static rendering&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Excellent SEO&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fast routing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Built-in image optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Incremental Static Regeneration (ISR)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tailwind CSS keeps styling consistent without maintaining a large CSS codebase.&lt;/p&gt;

&lt;p&gt;Deploying to Vercel makes the frontend almost effortless. Every push automatically builds and deploys the latest version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backend
&lt;/h2&gt;

&lt;p&gt;Instead of relying on Next.js API routes, I built a dedicated backend using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Node.js&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Express&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hosted on Render&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This backend acts as the single source of truth for all dynamic content.&lt;/p&gt;

&lt;p&gt;Whether it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;portfolio information&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;experience&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;projects&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;resume data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;future APIs&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;everything is served from one backend.&lt;/p&gt;

&lt;p&gt;Keeping the backend independent means it can later power:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;a mobile app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;an admin dashboard&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CLI tools&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;browser extensions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or any future frontend&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without changing business logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Database
&lt;/h2&gt;

&lt;p&gt;Dynamic content is stored in &lt;strong&gt;MongoDB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Documents include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Profile information&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Experience&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Skills&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Projects&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Portfolio data&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Interestingly, &lt;strong&gt;blog posts are not stored in MongoDB&lt;/strong&gt;. They are maintained separately, allowing the website content and blog content to evolve independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;To reduce unnecessary database queries, the Express API caches responses in Redis.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   │
   ▼
Express API
   │
   ▼
Redis
   │
Cache Hit?
   │
 ┌─┴─────────────┐
 │               │
Yes             No
 │               │
 ▼               ▼
Return      MongoDB
Response        │
                ▼
          Store in Redis
                │
                ▼
          Return Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most requests never reach MongoDB.&lt;/p&gt;

&lt;p&gt;This keeps API responses fast while reducing database load.&lt;/p&gt;




&lt;h1&gt;
  
  
  Overall Architecture
&lt;/h1&gt;

&lt;p&gt;At a high level, the system looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 +----------------------+
                 |      Next.js         |
                 |   Hosted on Vercel   |
                 +----------+-----------+
                            |
                            |
                     HTTP Requests
                            |
                            ▼
                +----------------------+
                |   Express Backend    |
                |   Hosted on Render   |
                +----------+-----------+
                           |
               +-----------+-----------+
               |                       |
               ▼                       ▼
        Redis Cache             MongoDB Atlas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although there are multiple services, each one has a single responsibility.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Next.js renders pages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Express serves business logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Redis caches responses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MongoDB stores data.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping responsibilities separate makes the system easier to reason about and easier to extend.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I Didn't Use Next.js API Routes
&lt;/h1&gt;

&lt;p&gt;This was probably the architectural decision that influenced the project the most.&lt;/p&gt;

&lt;p&gt;Many Next.js applications place all backend logic directly inside API routes.&lt;/p&gt;

&lt;p&gt;There's absolutely nothing wrong with that approach.&lt;/p&gt;

&lt;p&gt;But I wanted something reusable.&lt;/p&gt;

&lt;p&gt;By separating the backend, the frontend becomes just another client.&lt;/p&gt;

&lt;p&gt;Tomorrow, if I decide to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;an Android app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;an iOS app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a desktop application&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;another website&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;they can all consume the exact same API.&lt;/p&gt;

&lt;p&gt;No duplicated logic.&lt;/p&gt;

&lt;p&gt;No duplicated validation.&lt;/p&gt;

&lt;p&gt;No duplicated database queries.&lt;/p&gt;

&lt;p&gt;Everything lives in one place.&lt;/p&gt;




&lt;h1&gt;
  
  
  Making the Backend the Single Source of Truth
&lt;/h1&gt;

&lt;p&gt;The homepage.&lt;/p&gt;

&lt;p&gt;The portfolio.&lt;/p&gt;

&lt;p&gt;The CV.&lt;/p&gt;

&lt;p&gt;Future applications.&lt;/p&gt;

&lt;p&gt;All of them consume the same backend.&lt;/p&gt;

&lt;p&gt;Instead of every page maintaining its own copy of data, everything originates from one API.&lt;/p&gt;

&lt;p&gt;Updating my experience in MongoDB automatically updates every place where it's displayed.&lt;/p&gt;

&lt;p&gt;This significantly reduces maintenance and prevents data from going out of sync.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Unexpected Problem: Cache Invalidation
&lt;/h1&gt;

&lt;p&gt;The most interesting problem wasn't building the website.&lt;/p&gt;

&lt;p&gt;It was keeping it fresh.&lt;/p&gt;

&lt;p&gt;Initially, everything looked perfect.&lt;/p&gt;

&lt;p&gt;MongoDB stored the latest content.&lt;/p&gt;

&lt;p&gt;Redis cached API responses.&lt;/p&gt;

&lt;p&gt;Next.js generated static pages.&lt;/p&gt;

&lt;p&gt;Performance was excellent.&lt;/p&gt;

&lt;p&gt;Yet something strange happened.&lt;/p&gt;

&lt;p&gt;Whenever I updated content, users didn't always see the changes immediately.&lt;/p&gt;

&lt;p&gt;Sometimes it took several seconds.&lt;/p&gt;

&lt;p&gt;Sometimes much longer.&lt;/p&gt;

&lt;p&gt;At first, I assumed Redis was serving stale data.&lt;/p&gt;

&lt;p&gt;After debugging for a while, I realized Redis wasn't the problem at all.&lt;/p&gt;

&lt;p&gt;The real issue was that there were &lt;strong&gt;two completely independent caching layers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The API cache and the frontend cache.&lt;/p&gt;

&lt;p&gt;The API could already have fresh data while Next.js continued serving previously generated pages.&lt;/p&gt;

&lt;p&gt;Everything was technically working exactly as intended.&lt;/p&gt;

&lt;p&gt;The architecture, however, wasn't.&lt;/p&gt;




&lt;h1&gt;
  
  
  Solving It with Event-Driven Revalidation
&lt;/h1&gt;

&lt;p&gt;Instead of waiting for caches to expire naturally, I switched to an event-driven approach.&lt;/p&gt;

&lt;p&gt;Whenever content changes, the following sequence happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin API

      │

      ▼

Update MongoDB

      │

      ▼

Trigger Next.js Revalidation

      │

      ▼

Flush Redis Cache

      │

      ▼

Users receive fresh content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means content updates propagate almost immediately without waiting for cache expiration.&lt;/p&gt;

&lt;p&gt;The important lesson here is that &lt;strong&gt;caching is only half the problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The other half is knowing exactly &lt;strong&gt;when to invalidate that cache.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Designing a reliable invalidation strategy is often harder than adding caching in the first place.&lt;/p&gt;




&lt;h1&gt;
  
  
  Hosting Strategy
&lt;/h1&gt;

&lt;p&gt;Keeping the frontend and backend separate also simplified deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend
&lt;/h2&gt;

&lt;p&gt;The Next.js application is deployed on &lt;strong&gt;Vercel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automatic deployments&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Preview environments&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Global CDN&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optimized image delivery&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Backend
&lt;/h2&gt;

&lt;p&gt;The Express server runs independently on &lt;strong&gt;Render&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Separating deployments means I can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;deploy backend fixes without rebuilding the frontend&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;deploy UI updates without touching backend services&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;scale each independently in the future&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Lessons Learned
&lt;/h1&gt;

&lt;p&gt;Building this website taught me several lessons that extend far beyond personal projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Simplicity scales
&lt;/h2&gt;

&lt;p&gt;A small, well-structured architecture is easier to maintain than an unnecessarily complex one.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Separate responsibilities
&lt;/h2&gt;

&lt;p&gt;Frontend rendering, backend logic, caching, and persistence all have different jobs.&lt;/p&gt;

&lt;p&gt;Keeping those responsibilities isolated makes the system easier to evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Build reusable APIs
&lt;/h2&gt;

&lt;p&gt;The backend shouldn't exist solely for one website.&lt;/p&gt;

&lt;p&gt;Treating it as a standalone service opens the door for future applications without additional work.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Cache invalidation deserves as much attention as caching
&lt;/h2&gt;

&lt;p&gt;Adding Redis is easy.&lt;/p&gt;

&lt;p&gt;Designing when and how cached data should be refreshed is where the real engineering begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Personal projects are the best place to experiment
&lt;/h2&gt;

&lt;p&gt;Production systems often have strict requirements.&lt;/p&gt;

&lt;p&gt;Personal projects provide the freedom to test ideas, refine architectures, and learn from mistakes.&lt;/p&gt;

&lt;p&gt;Many of the lessons learned while building this website are directly applicable to larger production systems.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next?
&lt;/h1&gt;

&lt;p&gt;The website continues to evolve.&lt;/p&gt;

&lt;p&gt;Some ideas I'm exploring include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A richer admin experience for managing content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better analytics and monitoring&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search functionality&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI-powered features&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Additional APIs for future projects&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More automation around content publishing&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the architecture is modular, adding new capabilities doesn't require rewriting existing components.&lt;/p&gt;

&lt;p&gt;That's exactly how I wanted the system to grow.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Building &lt;a href="http://shubhkumar.in" rel="noopener noreferrer"&gt;&lt;strong&gt;shubhkumar.in&lt;/strong&gt;&lt;/a&gt; wasn't about creating another portfolio website.&lt;/p&gt;

&lt;p&gt;It was about building a platform that reflects how I think about software engineering.&lt;/p&gt;

&lt;p&gt;Choosing &lt;strong&gt;Next.js&lt;/strong&gt;, &lt;strong&gt;Tailwind CSS&lt;/strong&gt;, &lt;strong&gt;Node.js&lt;/strong&gt;, &lt;strong&gt;Express&lt;/strong&gt;, &lt;strong&gt;MongoDB&lt;/strong&gt;, and &lt;strong&gt;Redis&lt;/strong&gt; wasn't about following trends. It was about selecting tools that work well together while keeping the architecture clean and maintainable.&lt;/p&gt;

&lt;p&gt;The biggest lesson wasn't learning a new framework or deploying another application.&lt;/p&gt;

&lt;p&gt;It was realizing that good architecture isn't defined by how many technologies you use.&lt;/p&gt;

&lt;p&gt;It's defined by how clearly each piece of the system is responsible for one job and how well those pieces work together.&lt;/p&gt;

&lt;p&gt;If there's one takeaway I'd leave you with, it's this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat your personal projects like production systems.&lt;/strong&gt; Not because they need enterprise-scale complexity, but because they're the best place to learn the engineering practices you'll eventually use in production.&lt;/p&gt;

&lt;p&gt;After all, the best portfolio isn't the one with the fanciest animations it's the one that demonstrates how you think as an engineer.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>Moving from 60s to 6s: Latency Optimization Lessons from Functional Programming</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Wed, 24 Jun 2026 10:36:02 +0000</pubDate>
      <link>https://dev.to/shubham399/moving-from-60s-to-6s-latency-optimization-lessons-from-functional-programming-2l7i</link>
      <guid>https://dev.to/shubham399/moving-from-60s-to-6s-latency-optimization-lessons-from-functional-programming-2l7i</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2F0370399c-e484-465a-80ba-3bef2793cd94%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2F0370399c-e484-465a-80ba-3bef2793cd94%2Fpublic" width="760" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The broader tech community often views functional programming (FP) as an elegant academic exercise: useful for type systems, formal reasoning, and compiler guarantees, but distant from high-throughput production systems.&lt;/p&gt;

&lt;p&gt;That framing misses something important. FP can improve the way teams model asynchronous work, failures, and state transitions. But it is not a substitute for finding the actual source of latency.&lt;/p&gt;

&lt;p&gt;In a distributed workflow engine, we reduced observed end-to-end completion time from roughly 60 seconds to under 6 seconds for the common successful path. The primary cause was architectural: we removed repeated polling and queue wait from the synchronous execution path. PureScript and Haskell helped us express the resulting asynchronous flow explicitly and safely; they did not, by themselves, create a 10x latency improvement.&lt;/p&gt;

&lt;p&gt;This is the engineering story behind that change, the measurements it supports, and the tradeoffs it introduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers mean
&lt;/h2&gt;

&lt;p&gt;The 60-second and under-6-second figures are observed end-to-end timings for the workflow’s common success path, measured from request acceptance through the final response. They are not a universal service-level objective, a benchmark of every workflow type, or a claim about every percentile under every load level.&lt;/p&gt;

&lt;p&gt;For a production latency claim, the useful view is a before/after comparison with the same workload and scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; identical successful workflow type, including validation, business-rule evaluation, external calls, state update, and response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load:&lt;/strong&gt; compare equivalent request rate, worker availability, dependency health, and database conditions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distribution:&lt;/strong&gt; report p50, p95, and p99, along with sample size and the observation window, rather than relying on a single elapsed time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Boundaries:&lt;/strong&gt; state whether timings include client/network time, queue time, retries, and downstream-service time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, the roughly 60-second to under-6-second result should be read as an observed common-path improvement. The main lesson is diagnostic: most of the old latency was scheduled waiting, not useful computation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck: polling-based workflow execution
&lt;/h2&gt;

&lt;p&gt;The original system used a pull-based worker architecture. Each request moved through sequential stages: validation, business-rule evaluation, external-service interactions, state transitions, and final reconciliation.&lt;/p&gt;

&lt;p&gt;A database-backed work queue coordinated that workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A worker completed a step and persisted the updated state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A later worker polled the database for pending work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After discovering the work, it executed the next stage and persisted the result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The cycle continued until the workflow completed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design had real benefits. It made work durable, gave operators a visible recovery point, and supported retries. It also inserted a scheduling delay between stages. With several sequential transitions, those polling intervals and queue waits accumulated.&lt;/p&gt;

&lt;p&gt;The system was not primarily compute-bound. It was wait-bound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architectural change: a fast path and a durable path
&lt;/h2&gt;

&lt;p&gt;We separated the responsibilities that had previously been forced through one path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fast path:&lt;/strong&gt; execute the request directly when the workflow can complete synchronously.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Durable path:&lt;/strong&gt; retain queued execution for retries, recovery, delayed work, and cases that cannot safely finish inline.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A representative fast path is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Request → Validation → Business Rules → External Service Call → State Update → Response&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Instead of persisting and waiting for a poll between every step, the request continues through that chain while the required dependencies are available. Removing those handoffs is what removed the dominant source of delay.&lt;/p&gt;

&lt;p&gt;The queue was not eliminated because it was bad. It was moved out of the successful synchronous path because its durability and scheduling semantics were unnecessary for every transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where functional programming helped
&lt;/h2&gt;

&lt;p&gt;PureScript’s &lt;code&gt;Aff&lt;/code&gt; runtime gave the direct path a useful execution model: non-blocking asynchronous effects, composable sequencing, structured error handling, and cancellation/resource-safety primitives. Similar properties are available in other ecosystems; the language was an enabler, not the performance mechanism.&lt;/p&gt;

&lt;p&gt;FP techniques improved the implementation in three practical ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit effects:&lt;/strong&gt; database writes, remote calls, logging, and retries are visible in the program’s effectful boundary instead of being hidden in incidental control flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Typed outcomes:&lt;/strong&gt; expected failure modes can be represented as data, making it clearer which errors respond immediately, retry, or transfer to durable processing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Composable stages:&lt;/strong&gt; validation, rule evaluation, and external interactions can be assembled and tested as small units without scattering callback or exception handling across the workflow.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These properties made the fast path easier to reason about and operate. They did not compensate for a queueing architecture that was adding avoidable wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure handling and async decoupling tradeoffs
&lt;/h2&gt;

&lt;p&gt;The direct path makes a request faster by coupling more work to the request lifetime. That tradeoff needs to be deliberate.&lt;/p&gt;

&lt;p&gt;Queued workflows decouple producers from consumers, absorb bursts, provide durable handoff points, and allow retry/recovery to proceed after the original request has ended. A synchronous fast path gives up some of that decoupling in exchange for lower latency. It can increase pressure on downstream dependencies, expose callers to longer in-flight work, and require careful timeout, cancellation, idempotency, and backpressure policies.&lt;/p&gt;

&lt;p&gt;The design therefore needs a clear transfer rule. When the direct path encounters a retryable failure, an unavailable dependency, a deadline risk, or work that must outlive the request, it records enough durable state and hands the workflow to the durable path. That handoff must be idempotent so a timeout or ambiguous response does not duplicate an externally visible action.&lt;/p&gt;

&lt;p&gt;Useful safeguards include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;per-stage deadlines and bounded retries;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;idempotency keys for state-changing external calls;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;circuit breaking and concurrency limits around dependencies;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;durable audit records at defined commit points; and&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;separate metrics for direct completion, fallback, retry, and recovery outcomes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure the architecture, not just the aggregate
&lt;/h2&gt;

&lt;p&gt;Average latency can hide both queueing and tail failures. Instrument each transition so the system can distinguish queue wait, execution time, persistence time, and downstream-service time. Then compare p50, p95, and p99 before and after the change under matched load.&lt;/p&gt;

&lt;p&gt;For this workflow, the key measurement was not merely that a request became faster. It was that the old path spent substantial time waiting between otherwise short stages. That evidence justified changing the execution model. The percentile view then verifies whether the fast path improves typical and tail behavior, while fallback and error metrics show whether reliability has regressed.&lt;/p&gt;

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

&lt;p&gt;The useful conclusion is not that functional programming delivers a fixed latency multiplier. The observed reduction from roughly 60 seconds to under 6 seconds came primarily from removing polling and queue wait from the common successful path.&lt;/p&gt;

&lt;p&gt;Functional programming contributed by making the asynchronous orchestration, failure cases, and fallback boundary easier to express and review. The durable workflow system continued to matter for the work that needs decoupling, retries, and recovery.&lt;/p&gt;

&lt;p&gt;Find the waiting first. Then choose an architecture that removes unnecessary waiting while preserving the operational guarantees the workload actually requires.&lt;/p&gt;

</description>
      <category>functional</category>
      <category>programming</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Disconnected Edge: How We Solved In-Flight Data Sync at 35,000 Feet</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 14 Jun 2026 01:55:15 +0000</pubDate>
      <link>https://dev.to/shubham399/the-disconnected-edge-how-we-solved-in-flight-data-sync-at-35000-feet-4baf</link>
      <guid>https://dev.to/shubham399/the-disconnected-edge-how-we-solved-in-flight-data-sync-at-35000-feet-4baf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fb805ea06-69a6-46ed-8545-f24e8e98ae9e.png%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fb805ea06-69a6-46ed-8545-f24e8e98ae9e.png%2Fpublic" alt="Offline-first in-flight data synchronization architecture" width="1344" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most application architectures assume the network is available often enough to repair mistakes: fetch fresh configuration, retry an API call, consult a central database, or stream a missing asset from a CDN.&lt;/p&gt;

&lt;p&gt;An aircraft edge system does not get that safety net. For long stretches, the onboard platform must operate as if the backend does not exist. When a connection appears, it may be short, expensive, slow, or interrupted halfway through a transfer.&lt;/p&gt;

&lt;p&gt;That changed how we designed an in-flight entertainment platform. The hard part was not serving movies, games, catalogs, and passenger experiences locally. The hard part was moving the right data between a central backend and intermittently connected aircraft without leaving either side in an ambiguous state.&lt;/p&gt;

&lt;p&gt;This is the offline-first model we used: make the aircraft independently useful, treat synchronization as a deliberate protocol rather than a background convenience, and make every partial failure recoverable. Specific identifiers and thresholds here are illustrative; the design principles are the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operating environment: disconnected by default
&lt;/h2&gt;

&lt;p&gt;Each aircraft carried an embedded edge system responsible for the passenger experience: media, applications, digital publications, catalogs, configuration, and operational telemetry. It had local storage and local services, but no guarantee of a usable path to the internet.&lt;/p&gt;

&lt;p&gt;That creates constraints that are easy to underestimate from a cloud-first mindset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Connectivity windows are intermittent and may end without warning.&lt;/li&gt;
&lt;li&gt;  Bandwidth can be scarce, variable, and costly.&lt;/li&gt;
&lt;li&gt;  Devices must keep serving known-good content while updates are incomplete.&lt;/li&gt;
&lt;li&gt;  A reboot, power event, or failed transfer cannot corrupt the active passenger experience.&lt;/li&gt;
&lt;li&gt;  Central systems must distinguish “not yet uploaded” from “lost forever.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend remained authoritative for centrally managed content, configuration, and policy. But the aircraft had to be operationally autonomous. Offline mode was not a degraded fallback; it was the normal mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the data lanes before designing sync
&lt;/h2&gt;

&lt;p&gt;“Sync everything” is not a protocol. Different kinds of data have different correctness rules, sizes, and priorities. We treated them as separate lanes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Content:&lt;/strong&gt; large immutable media and application assets. Correctness means every byte matches a known version.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Configuration:&lt;/strong&gt; smaller, product-sensitive settings. Correctness means a complete, compatible version is activated atomically.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Telemetry and analytics:&lt;/strong&gt; append-only events generated onboard. Correctness means no silent loss and no harmful double counting.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Operational commands:&lt;/strong&gt; centrally issued intent, such as a requested content set. Correctness means explicit acknowledgement and an auditable lifecycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the lanes were separated, we could give each one a suitable delivery, retry, and conflict model instead of forcing all data through one generic “sync” abstraction. It also gave the scheduler useful priorities: a small compatible configuration update can be more valuable than the next chunk of a large optional asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make sync a persisted state machine
&lt;/h2&gt;

&lt;p&gt;A sync worker that only lives in memory is fragile. It forgets why it stopped after a reboot, cannot distinguish a paused transfer from a failed one, and makes recovery dependent on log archaeology. We modeled synchronization as a small persisted state machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idle → discovering → planning → transferring → verifying
   ↑                                      ↓
   └──── paused / retry_wait ← activating ← ready
                              ↓
                         quarantined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact labels are less important than the invariant: every transition is durable and restartable. The device records the active release, requested release, manifest version, compatibility result, artifact/chunk progress, retry schedule, event-upload cursor, and the reason for any terminal failure. On startup, the worker reads that state and resumes conservatively rather than guessing from partial files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"activeRelease"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-2026-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"desiredRelease"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-2026-06"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"syncState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transferring"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifestHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verifiedBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;734003200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventCursor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;817291&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nextRetryAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-13T08:15:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persisted state also creates a useful boundary between mechanism and policy. The transfer mechanism knows whether a chunk is verified. Policy decides whether to retry now, defer a nonessential asset, quarantine a release, or ask an operator to investigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioned manifests made content deterministic
&lt;/h2&gt;

&lt;p&gt;For content and application bundles, we avoided asking the device to infer what changed from a directory listing. The backend produced an immutable, versioned manifest describing the desired release: asset identifiers, sizes, hashes, dependencies, compatibility requirements, and configuration version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"releaseId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ife-2026.06.13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minPlatformVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4.8.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"movies/example.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1789423412&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"configVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cfg-2026-06-13-02"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An edge device first fetched the small manifest, checked signature and compatibility, compared it with local state, and then downloaded only missing or changed artifacts. Every artifact was verified before it could be considered ready. A successful HTTP response was not proof of correctness; the expected hash was.&lt;/p&gt;

&lt;p&gt;Large files were transferred in chunks with persisted progress. A connection loss simply paused work at the latest verified boundary. On the next window, the device resumed instead of starting again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for each required asset:
  read verified chunk offsets from local state
  request missing ranges
  verify each completed chunk
  verify final asset hash
  mark asset ready only after verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compatibility is part of correctness. A manifest can demand a platform version, a schema version, or a migration path that the device does not support. In that case the worker must reject the release explicitly and report why; silently applying a newer structure to an older runtime creates harder failures later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never activate a half-synced release
&lt;/h2&gt;

&lt;p&gt;Downloading a release and serving it are separate operations. New artifacts were staged outside the active content set. Only when the entire manifest was present, verified, compatible, and accompanied by a valid configuration did the device switch the active pointer in one durable operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN TRANSACTION;
  assert release is complete and verified;
  assert configuration is compatible;
  set active_release = :releaseId;
  record activated_at = :timestamp;
COMMIT;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the key recovery property: a failed or interrupted sync leaves the previous known-good release active. The next sync can resume staging. It never turns an incomplete directory into the passenger experience.&lt;/p&gt;

&lt;p&gt;Rollback followed the same model. Retaining a previous verified release made recovery a pointer change rather than an emergency re-download. The device reported both its desired release and active release so the backend could distinguish “download in progress” from “activation failed.”&lt;/p&gt;

&lt;h2&gt;
  
  
  An end-to-end sync window
&lt;/h2&gt;

&lt;p&gt;A typical connection window followed a deliberately boring sequence. First, the device authenticated and sent a compact status summary: software version, active release, desired-release status, storage pressure, event cursor, and the outcome of the prior attempt. The backend replied with policy and the latest eligible manifest.&lt;/p&gt;

&lt;p&gt;Next, the device planned work. It verified that the release was compatible and that there was enough staging capacity. It then prioritized small metadata, critical configuration, and pending commands before large content artifacts. Meanwhile, telemetry upload ran in bounded batches so that a large backlog could not starve a critical update, and a big download could not starve telemetry indefinitely.&lt;/p&gt;

&lt;p&gt;During transfer, every request had a deadline. Completed ranges were recorded only after verification. If the connection disappeared, the worker retained its exact state and backed off until the next viable attempt. If all assets passed verification, activation was a separate, short transaction. Finally, the device sent an acknowledgement containing the active release and any rejected or quarantined items.&lt;/p&gt;

&lt;p&gt;That acknowledgement closed the loop. The backend could not infer success from having served a manifest. A release was operationally complete only after the device reported it active.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload events as an idempotent append-only stream
&lt;/h2&gt;

&lt;p&gt;Telemetry and passenger analytics are fundamentally different from content. They are produced locally while disconnected, then uploaded later. The edge system persisted events before attempting delivery and assigned each event a stable identity, such as a device id plus a monotonic sequence number or a generated UUID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deviceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aircraft-edge-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;817292&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01J…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"content_started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occurredAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-13T08:12:24Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"contentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"movie-123"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend accepted batches idempotently. If a connection failed after the server accepted a batch but before the aircraft received acknowledgement, retrying the same events was safe because the backend could recognize identities it had already processed.&lt;/p&gt;

&lt;p&gt;Server acknowledgements advanced a durable cursor only after accepted events were recorded. The edge node retained data until that acknowledgement was committed, then compacted acknowledged records according to retention policy. This produces at-least-once transport with effectively-once accounting when the consumer deduplicates by event identity.&lt;/p&gt;

&lt;p&gt;Not every failure deserves an infinite retry. A temporarily unavailable endpoint may be retried with backoff. A malformed event, unsupported schema, or permanently rejected payload should move to a quarantine or dead-letter record with a reason, preserving evidence without blocking the entire queue. Operators can inspect, repair, discard, or replay it through an explicit process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflicts need a taxonomy, not a universal CRDT
&lt;/h2&gt;

&lt;p&gt;Offline systems do create conflicts, but “use CRDTs” is not a complete answer. A conflict policy should follow ownership and business semantics.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Server-authoritative data:&lt;/strong&gt; release manifests, pricing policy, and centrally managed configuration should have a single authority. The device applies a compatible version; it does not merge edits.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Append-only facts:&lt;/strong&gt; telemetry is normally merged by deduplication, ordering metadata, and domain-specific aggregation not by overwriting records.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Locally authored mutable state:&lt;/strong&gt; if multiple offline writers can independently edit the same logical object, a CRDT may be appropriate when its merge semantics match the product. Counters, sets, and collaborative metadata are possible examples.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Irreconcilable changes:&lt;/strong&gt; some domains require explicit rejection or human review. Last-write-wins is a policy choice, not a conflict-resolution strategy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CRDTs are valuable because they can guarantee convergence under particular operations and merge rules. They do not create correct business semantics automatically. For most centrally controlled data, immutable versions plus server authority were easier to reason about and audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bandwidth, storage, and rollout policy
&lt;/h2&gt;

&lt;p&gt;A device did not attempt every task whenever a network interface looked available. It assessed the connection, applied policy, and worked through a priority queue. Small manifests and critical configuration came first; event uploads and large optional artifacts were scheduled according to remaining budget and product priority.&lt;/p&gt;

&lt;p&gt;Transfers used bounded concurrency, request deadlines, exponential backoff with jitter, and persisted state. Retrying blindly can consume an entire connectivity window, so retries were capped and re-evaluated when conditions changed. Each request carried enough identity to make retries observable and safe.&lt;/p&gt;

&lt;p&gt;Storage required policy too. The active release and a known-good rollback release were protected from normal eviction. Incomplete staging content could be removed safely when it no longer matched the desired manifest. Optional, verified assets could be evicted only according to explicit rules, never based on a generic filesystem cleanup that might remove an activation dependency.&lt;/p&gt;

&lt;p&gt;Rollouts were also progressive. A new release should first be eligible for a limited ring of devices, then expand only when acknowledgements, validation results, and operational signals remain healthy. A spike in compatibility failures, verification errors, activation failures, or abnormal backlog growth should pause expansion. The design needs a clear rollback path before a release is ever offered to a wider fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security, credentials, and privacy at the edge
&lt;/h2&gt;

&lt;p&gt;An offline device still needs a trust model. Transport encryption protects an available connection, but it does not prove that a downloaded artifact is an approved release. Manifests and artifacts need integrity verification; depending on the threat model, signed manifests provide stronger provenance than hashes fetched from the same channel as the content.&lt;/p&gt;

&lt;p&gt;Devices should authenticate with distinct, scoped, revocable identities. Credential rotation needs a tolerable overlap period: the device validates a replacement credential, records it durably, and retains the old one only long enough to avoid being stranded during an interrupted rotation. Revocation and expiry must be visible in device state and backend operations.&lt;/p&gt;

&lt;p&gt;Data minimization matters as much as transport security. Sync only data the aircraft needs to operate, collect only telemetry necessary for product and operational use, and define retention boundaries for local queues and backend ingestion. Avoid logging access tokens, passenger-sensitive data, or complete event bodies when metadata will do. Backend authorization should scope every request to the device and fleet it is permitted to access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations: measure partial progress
&lt;/h2&gt;

&lt;p&gt;Intermittent systems need better observability than a simple “online” metric. We tracked release state, manifest version, bytes remaining, verified chunks, last successful contact, event backlog age, upload acknowledgement cursor, retry counts, active release, failed validation reasons, and storage pressure.&lt;/p&gt;

&lt;p&gt;Those signals made it possible to answer useful operational questions: which devices are running an old release, which are stuck on a corrupt artifact, which have an increasing analytics backlog, and whether a rollout problem is global or isolated to a connection path.&lt;/p&gt;

&lt;p&gt;The health objective is not constant connectivity. It is eventual convergence without harming the active experience: a device should remain useful on a known-good release, make measurable progress when a connection exists, and surface failures that need intervention. Recovery paths were deliberate: stale transfer leases could be reclaimed, incomplete staging directories could be cleaned safely, old verified releases could be rolled back to, and event batches could be replayed without double counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure modes, not only the protocol
&lt;/h2&gt;

&lt;p&gt;The system needed tests for conditions that are rare in a development environment but routine at the edge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Connection loss during manifest retrieval, range download, upload, and acknowledgement.&lt;/li&gt;
&lt;li&gt;  Process restart or power loss before and after an active-release pointer change.&lt;/li&gt;
&lt;li&gt;  Duplicate event batches, reordered responses, and stale acknowledgements.&lt;/li&gt;
&lt;li&gt;  Corrupted chunks, incompatible manifests, expired credentials, and full disks.&lt;/li&gt;
&lt;li&gt;  Clock drift, long offline periods, and recovery after a device misses multiple releases.&lt;/li&gt;
&lt;li&gt;  Stale locks, interrupted credential rotation, and release rollback during an active sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fault injection was more useful than happy-path tests alone. We simulated slow links, short connections, partial range responses, duplicate uploads, backend timeouts, and storage exhaustion. The invariant under test was simple: after any interruption, the device must either continue serving the last verified release or recover to a well-defined state without inventing completion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in our design process
&lt;/h2&gt;

&lt;p&gt;Building for aircraft made us stop treating the network as a dependable dependency. The device had to remain useful with a stale but verified local state. The backend had to accept delayed, duplicate, and partial communication without losing its understanding of the fleet.&lt;/p&gt;

&lt;p&gt;That led to a few durable rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Make offline operation a first-class product requirement.&lt;/li&gt;
&lt;li&gt; Persist sync state; never rely on a process remembering where it was.&lt;/li&gt;
&lt;li&gt; Use immutable manifests and hashes for large artifacts.&lt;/li&gt;
&lt;li&gt; Stage, verify, and atomically activate releases; retain a known-good rollback target.&lt;/li&gt;
&lt;li&gt; Persist outbound events and make server ingestion idempotent.&lt;/li&gt;
&lt;li&gt; Define conflict policy by ownership and domain semantics; use CRDTs only where their merge model fits.&lt;/li&gt;
&lt;li&gt; Prioritize work intentionally across bandwidth and storage constraints.&lt;/li&gt;
&lt;li&gt; Measure partial progress, backlog age, and recovery not only availability.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Offline-first synchronization is not a smaller version of cloud sync. It is a distributed-systems problem where partitions are normal and recovery must be designed before the failure happens.&lt;/p&gt;

&lt;p&gt;For an in-flight edge platform, immutable releases, resumable verified transfer, atomic activation, idempotent event delivery, explicit conflict rules, and clear operational state made the system dependable even when connectivity was not. Those patterns apply far beyond aircraft: ships, retail stores, factories, field devices, and any product that must keep working when the network disappears.&lt;/p&gt;

</description>
      <category>edgecomputing</category>
      <category>distributedsystems</category>
      <category>offlinefirst</category>
    </item>
    <item>
      <title>Turning Your AI Into an Adversarial Security Agent: The SKILLS.md Framework</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 07 Jun 2026 09:18:06 +0000</pubDate>
      <link>https://dev.to/shubham399/turning-your-ai-into-an-adversarial-security-agent-the-skillsmd-framework-2058</link>
      <guid>https://dev.to/shubham399/turning-your-ai-into-an-adversarial-security-agent-the-skillsmd-framework-2058</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2F3f9a92b5-1a1a-4de4-ab07-e5b347d1b179.png%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2F3f9a92b5-1a1a-4de4-ab07-e5b347d1b179.png%2Fpublic"&gt;&lt;/a&gt;A continuation of: &lt;a href="https://www.shubhkumar.in/blogs/breaking-to-build-how-ctf-and-bug-bounty-hunting-rewires-system-design" rel="noopener noreferrer"&gt;&lt;em&gt;Breaking to Build: How CTF and Bug Bounty Hunting Rewires System Design&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my previous article, I explored how offensive security permanently changes the way engineers think about systems. Once you've spent enough time exploiting race conditions, bypassing authorization boundaries, abusing SSRF chains, and breaking assumptions hidden deep inside application logic, you stop viewing software as a collection of features.&lt;/p&gt;

&lt;p&gt;You start viewing it as an &lt;strong&gt;attack surface&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That shift fundamentally changes how you design production systems. The problem is that modern software development is no longer purely human-driven. Today, a massive percentage of engineering work happens alongside AI coding assistants. Tools now generate thousands of lines of code faster than most engineers can review them.&lt;/p&gt;

&lt;p&gt;And that introduces a brand new problem.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI systems are optimized for one thing:&lt;/strong&gt; Generate code that works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Attackers are optimized for something completely different:&lt;/strong&gt; Find code that breaks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That difference matters. A generated API endpoint might pass every functional test while still exposing a devastating BOLA (Broken Object Level Authorization) vulnerability. A generated webhook handler might function perfectly while allowing SSRF into your internal infrastructure. A generated payment workflow might appear correct while collapsing into a double-spend condition under concurrent execution.&lt;/p&gt;

&lt;p&gt;The code works. The architecture fails. And that is exactly where real-world vulnerabilities are born.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing Layer in AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;Most teams currently treat AI coding agents like extremely fast junior engineers. They give them instructions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"Build this feature"&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"Refactor this service"&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"Create this migration"&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model responds by optimizing for correctness, readability, and implementation speed. Security is rarely treated as a first-class objective.&lt;/p&gt;

&lt;p&gt;Most AI systems are never explicitly taught to think like attackers. They are taught how software &lt;em&gt;should&lt;/em&gt; behave; they are not taught how software is &lt;em&gt;abused&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That distinction becomes increasingly dangerous as organizations move toward autonomous code generation, AI-assisted architecture, and agentic development workflows.&lt;/p&gt;

&lt;p&gt;The solution turns out to be surprisingly simple: instead of prompting for features alone, we inject a persistent security reasoning framework directly into the agent's operating context.&lt;/p&gt;

&lt;p&gt;That framework is &lt;strong&gt;SKILLS.md&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is SKILLS.md?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SKILLS.md&lt;/code&gt; is a structured operational framework that teaches an AI agent how to evaluate software through an adversarial lens. It is not a prompt, a simple checklist, or another copy-paste of the OWASP Top 10. It is a behavioral framework that continuously pushes the model to ask &lt;strong&gt;"How would an attacker abuse this?"&lt;/strong&gt; &lt;em&gt;before&lt;/em&gt; it asks &lt;strong&gt;"How do I implement this?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The goal is to transplant the mindset developed through years of CTF competitions, bug bounty hunting, and incident response directly into the AI’s reasoning process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Traditional Security Checklists Fail
&lt;/h3&gt;

&lt;p&gt;Most security documentation focuses on known vulnerability categories (XSS, SQLi, CSRF, SSRF, IDOR). These are important, but attackers rarely think in categories. &lt;strong&gt;They think in assumptions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every vulnerability exists because somebody assumed something was true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;The frontend won't send invalid values.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Only authenticated users can reach this endpoint.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;This request executes once at a time.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Nobody can access that internal network.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bug bounty hunting teaches you something uncomfortable: assumptions are where systems fail. Security is often less about blocking payloads and more about eliminating dangerous assumptions. &lt;code&gt;SKILLS.md&lt;/code&gt; is built entirely around that philosophy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evolution From Builder To Breaker
&lt;/h2&gt;

&lt;p&gt;Plaintext&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional Engineering:
Requirement ──&amp;gt; Implementation ──&amp;gt; Testing ──&amp;gt; Deployment

Security-Oriented Engineering:
Requirement ──&amp;gt; Implementation ──&amp;gt; Abuse Analysis ──&amp;gt; Boundary Verification ──&amp;gt; Concurrency Analysis ──&amp;gt; Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first workflow asks: &lt;em&gt;Does this feature work?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The second asks: &lt;em&gt;What happens when somebody intentionally tries to break it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SKILLS.md&lt;/code&gt; forces AI agents into the second mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Specifications: SKILLS.md
&lt;/h2&gt;

&lt;p&gt;Modern AI tools and tools like &lt;strong&gt;Claude Code&lt;/strong&gt; have evolved past static, single-file home directory configurations. They utilize the &lt;strong&gt;Agent Skills Standard&lt;/strong&gt;, which relies on a nested folder footprint (&lt;code&gt;skills/&amp;lt;skill-name&amp;gt;/SKILL.md&lt;/code&gt;) and mandatory &lt;strong&gt;YAML frontmatter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The frontmatter contains semantic metadata. When you start an AI session, the engine scans the &lt;code&gt;description&lt;/code&gt; block to automatically determine &lt;em&gt;when&lt;/em&gt; to pull this skill into context.&lt;/p&gt;

&lt;p&gt;Here is the production-ready implementation file.&lt;/p&gt;

&lt;p&gt;Markdown&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secure&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluates&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;software&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;architecture&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;through&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;adversarial&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lens.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Automatically&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;invokes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;generating&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;APIs,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;designing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;features,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reviewing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;managing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;authentication,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;state,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;boundaries."&lt;/span&gt;
&lt;span class="na"&gt;user-invocable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;# Security-First Architecture Skill&lt;/span&gt;

&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*Axiom&lt;/span&gt;&lt;span class="s"&gt;:** Inputs malicious. Clients untrusted. Networks hostile. Dependencies may be compromised. Never trust; always verify at execution point.&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;## Domain Controls&lt;/span&gt;

&lt;span class="pi"&gt;|&lt;/span&gt; &lt;span class="c1"&gt;# | Domain | Attacks | Key Controls | Core Question |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt;---|--------|---------|-------------|---------------|&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 1 | **State / Race** | TOCTOU, double-spend, optimistic-lock loss | `SELECT FOR UPDATE`; distributed lock (Redis `SET NX PX`); validate ETag every write | Can same op succeed twice in parallel, or state change between check and act? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 2 | **AuthZ / BOLA** | IDOR, BOLA, mass assignment, GraphQL introspection | Ownership check at data layer, not route; allowlist binding (strong_params/Pydantic DTO); disable introspection in prod; tenant-scope every query | What changes if resource ID or any request field changes? Who verified ownership? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 3 | **SSRF** | Metadata endpoint, DNS rebind, redirect chain, `gopher://` | Resolve DNS → re-validate IP vs RFC-1918+169.254+fc00 denylist; allowlist domains; HTTPS-only via egress proxy; proxy follows redirects, not app code | Who controls final destination after DNS resolution and redirects? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 4 | **Path Traversal** | `../../`, URL-encode, null byte, Zip Slip, symlink | `realpath()`/`Path.resolve()` → verify under root; never concat user input to paths; use UUIDs as storage keys; validate archive entries before extract | Can user-controlled string, after normalization, escape storage boundary? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 5 | **Blast Radius** | Lateral movement, over-permissive IAM, credential reuse | Non-root containers, read-only rootfs, `--cap-drop ALL`; per-service least-privilege IAM; separate creds per env/service; mTLS between services | If this service is fully compromised, what else is reachable without new creds? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 6 | **Fail-Closed** | Broad catch-continue, feature-flag default-on, null bypass | Default DENY in every auth check, exception block, conditional; feature flags off for security features; `default: deny` in all security-relevant switches | What does system permit on unexpected error, null, or undefined in auth path? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 7 | **Secrets / Crypto** | Log leak, `alg:none` JWT, weak HMAC, IV reuse, timing oracle, weak PRNG | Secrets Manager + rotation; CSPRNG only; pin JWT alg server-side; RS256/ES256 cross-service; `timingSafeEqual`; AES-256-GCM unique nonces; ban MD5/SHA-1/DES/RC4 | Can credential be recovered, forged, or brute-forced from token, log, or build output? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 8 | **Supply Chain** | Typosquat, dep-confusion, `postinstall` RCE, unpinned deps | Exact-version pin + lockfile; verify signatures/hashes; audit `postinstall` scripts; private registry namespace; `npm audit`/`pip-audit`/`cargo audit` in CI | What third-party code executes in build/runtime that we don't own and audit? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 9 | **Event Integrity** | Replay, out-of-order, schema-invalid crash, webhook spoof | Idempotency keys (Redis TTL); strict schema → DLQ on malformed; HMAC-SHA256 webhook sig + 5-min timestamp window; sequence numbers for ordering | Can replaying/reordering an event corrupt state? Is every inbound event cryptographically authenticated? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 10 | **LLM / AI** | Prompt injection (direct/indirect), agentic tool abuse, secondary injection | Treat model output as untrusted input; explicit tool-auth gateway per user permission; strict output schema (JSON Schema/Pydantic); human-in-loop for irreversible actions; no ambient creds in agent env | What auth boundaries exist between model output and execution? Can injected content in retrieved data override system instructions? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 11 | **Injection** | SQLi, XSS, CMDi, SSTI, XXE, NoSQLi, ReDoS | Parameterized queries; `execFile([...])` not `exec(string)`; context-aware output encoding; disable XML external entities; reject `$`-prefix JSON keys; audit regexes for backtracking; never eval user input in templates | Does any user-controlled string reach an interpreter without structural separation? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 12 | **AuthN / Session / OAuth** | Credential stuffing, session fixation, missing server-side invalidation, redirect-URI wildcard, missing PKCE, token-in-URL | CSPRNG session tokens; rotate on login/priv-escalation; server-side invalidation on logout; `Secure;HttpOnly;SameSite=Strict`; Argon2id (64MB/3-iter) or bcrypt≥12; exact-match `redirect_uri`; PKCE for public clients; bind `state` to session; identical error messages + timing | Can attacker reuse, predict, fix, or intercept session/token/auth-code without knowing original secret? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 13 | **Log Exposure** | Credential leak, PII capture, log forgery via `\n\r`, stack-trace disclosure | Scrub tokens/JWTs/keys/PAN/SSN/email before every log write; allowlist loggable fields; sanitize `\n\r\033` in user input; disable verbose stack traces in prod responses | If every log line leaked, what sensitive data would be visible? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 14 | **Deserialization** | pickle/ObjectInputStream/`yaml.load`/Marshal RCE, gadget chains, DoS via nested structures | Never deserialize untrusted data with native formats; use JSON/Protobuf + schema validation; if unavoidable: allowlist filter + HMAC-sign payload; always `yaml.safe_load` | Does any code path deserialize attacker-influenced data with a class-instantiating deserializer? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 15 | **Rate Limits / DoS** | Credential stuffing, ReDoS, zip bomb, billion-laughs, large upload, unbounded pagination | Rate limits per-IP + per-user at gateway and app; tightest on auth/reset/OTP; cap body/upload size at ingress; GraphQL depth+complexity limits; timeouts on all external calls + queries; queue expensive ops; `limit ≤ 100` on pagination; return 429 + `Retry-After` | Can a single actor trigger resource consumption that degrades availability for others? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 16 | **HTTP Controls** | CORS credential theft, XSS via missing CSP, clickjacking, MIME-sniff bypass, SSL-strip | Exact-origin CORS allowlist (never reflect `Origin`, never `*` + credentials); HSTS `max-age=63072000;includeSubDomains;preload`; CSP allowlist `script-src`, no `unsafe-inline/eval`; `X-Content-Type-Options:nosniff`; `X-Frame-Options:DENY`; `Referrer-Policy:strict-origin-when-cross-origin`; `__Host-` cookie prefix | Can cross-origin page, framed page, or MIME-sniffed resource exploit browser trust in this origin? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 17 | **File Upload** | Zip Slip, polyglot exec, oversized upload, SVG XSS, archive bomb | UUID storage keys (never user filename); separate origin (S3 bucket/CDN subdomain) + `Content-Disposition:attachment`; validate by magic bytes not MIME; enforce size+count at ingress; reject SVG/HTML or sanitize with DOMPurify; cap archive extraction size + entry count | Can an uploaded file execute code, escape storage, or gain application-origin trust? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 18 | **Info Disclosure** | Stack traces, version headers, debug endpoints, username enum, timing leak | Opaque error IDs to clients, full detail server-side only; remove `Server`/`X-Powered-By`/`X-AspNet-Version`; disable debug/admin/introspection in prod (fail startup if debug=prod); identical error messages + response times; scan for exposed debug routes | Does any response, header, error, or timing difference reveal internal structure to unauthorized caller? |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; 19 | **Build Pipeline** | Compromised CI, over-permissive build IAM, unsigned images, secrets in logs, fork PR secret leak | Pin CI actions to commit SHAs; secrets only on protected branches; read-only source + write-only artifact IAM for build; sign images (cosign/Sigstore) + verify at deploy; OIDC ephemeral creds (no long-lived keys); branch protection: review + CI + signed commits required | Can compromised dependency update, CI job, or PR introduce malicious code reaching production without human review? |&lt;/span&gt;

&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="s"&gt;# Verification Protocol (run sequentially on every review/design)&lt;/span&gt;

&lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="s"&gt;. **Trust Boundaries**   map all input sources (HTTP, queues, webhooks, files, env); strip all safety assumptions&lt;/span&gt;
&lt;span class="err"&gt;2&lt;/span&gt;&lt;span class="s"&gt;. **Concurrency**   find TOCTOU windows; verify DB-level locking on every state mutation&lt;/span&gt;
&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="s"&gt;. **AuthZ**   confirm ownership check at data-access layer, not route layer, on every request&lt;/span&gt;
&lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="s"&gt;. **Privileges**   least-privilege IAM, service accounts, container caps, DB permissions&lt;/span&gt;
&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="s"&gt;. **Blast Radius**   full compromise simulation: map reachable services/creds/data without new credentials&lt;/span&gt;
&lt;span class="err"&gt;6&lt;/span&gt;&lt;span class="s"&gt;. **Interpreter Paths**   trace user input to SQL/shell/template/XML/YAML/pickle; confirm structural separation&lt;/span&gt;
&lt;span class="err"&gt;7&lt;/span&gt;&lt;span class="s"&gt;. **Auth Surface**   session lifecycle (issue→rotate→invalidate), MFA, OAuth parameter binding, credential storage&lt;/span&gt;
&lt;span class="err"&gt;8&lt;/span&gt;&lt;span class="s"&gt;. **Log Audit**   confirm no secrets/PII/payment/raw bodies in any log path including error handlers and APM agents&lt;/span&gt;
&lt;span class="err"&gt;9&lt;/span&gt;&lt;span class="s"&gt;. **Rate Controls**   sensitive endpoints have per-user + per-IP limits; expensive ops queued with concurrency caps&lt;/span&gt;
&lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="s"&gt;0. **HTTP Controls**   CORS allowlist, security headers, CSP, no verbose error leakage on all response paths&lt;/span&gt;
&lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="s"&gt;1. **Upload &amp;amp; Deserialization**   files stored outside app origin with server-generated keys; no native deserializer on untrusted data&lt;/span&gt;
&lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="s"&gt;2. **Build Pipeline**   CI secrets scoped, actions SHA-pinned, images signed and verified at deploy&lt;/span&gt;

&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; **Target:** Code that behaves predictably when an adversary is actively attempting to shatter it.&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installation Guide
&lt;/h2&gt;

&lt;p&gt;To ensure your AI assistant picks up this framework without breaking file path scopes, use the explicit terminal setups below depending on your favorite environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Claude Code
&lt;/h3&gt;

&lt;p&gt;Claude Code evaluates configurations from your global home configuration space (&lt;code&gt;~/.claude&lt;/code&gt;) or local workspaces (&lt;code&gt;.claude&lt;/code&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Global Installation&lt;/strong&gt; &lt;em&gt;(Applies across all code repositories on your machine without altering git states)&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;Bash&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p ~/.claude/skills/security-review
# Save the Markdown block above into this file:
nano ~/.claude/skills/security-review/SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;**&lt;/span&gt;Project-Specific Installation&lt;span class="k"&gt;**&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;Committed directly into git to enforce security rules across the whole engineering team&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
  mkdir -p .claude/skills/security-review&lt;br&gt;
  nano .claude/skills/security-review/SKILL.md&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### 2\. Cursor (and custom IDEs)

Cursor indexes markdown definitions gracefully via workspace indexing or dedicated custom instructions.

Bash

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

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;br&gt;
mkdir -p .cursor/skills/security-review&lt;br&gt;
nano .cursor/skills/security-review/SKILL.md&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
_(Alternatively, you can save it as a top-level_ `SKILLS.md` _file in your root workspace)._

### 3\. Orchestrated Agent Frameworks (CrewAI / LangGraph)

For autonomous multi-agent pipelines, pass the file directly as system background data inside your orchestration configuration:

YAML

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

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
yaml&lt;br&gt;
agent:&lt;br&gt;
  role: Adversarial Security Auditor&lt;br&gt;
  backstory: You analyze architectural code changes strictly through the lens of SKILLS.md rules.&lt;br&gt;
  instructions:&lt;br&gt;
    - Ingest the custom SKILLS.md baseline constraints.&lt;br&gt;
    - Check every generated code route against Concurrency and Trust Boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## How to Use the Framework

Once installed, you don’t need to repeatedly copy-paste security prompts. The framework leverages both passive and active execution behaviors.

### Method A: Automated Semantic Triggering (Passive Mode)

Because the custom frontmatter contains a deep `description` string, the AI continuously evaluates your inputs. If you type a standard prompt that crosses defensive boundaries, the engine auto-activates the skill behind the scenes.

*   **Your Prompt:** _"Write an endpoint that takes a user's uploaded image URL, downloads it, and processes metadata."_

*   **The AI's Internal Action:** The engine intercepts words like _URL_ and _downloads_. It auto-loads `security-review` from disk, catches the **SSRF / Deterministic Routing** rule, and adds domain validation code before outputting the feature.


### Method B: Manual Slash Invocation (Active Mode)

If you want to explicitly mandate an application review, call the skill directly via standard interface paths.

*   **In Claude Code:** Use the custom command shortcut directly inside your terminal session:

    Bash

    ```


    /security-review Review our new database migration file for potential data isolation vulnerabilities.


    ```


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

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In Cursor Composer:&lt;/strong&gt; Force index mapping by targeting the file handle directly inside the chat bar:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Please build out our stripe payment callback router following the criteria defined in @SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Transformations: Before and After
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;SKILLS.md&lt;/code&gt; is active, the agent stops acting like a passive code generator and starts acting like an unyielding architecture reviewer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Payment Balance Deduction
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Without SKILLS.md:&lt;/strong&gt; The user asks for a simple point redemption function. The AI generates a standard &lt;code&gt;SELECT balance&lt;/code&gt; followed by an &lt;code&gt;UPDATE balance&lt;/code&gt; sequence. It looks clean, passes unit tests, but immediately falls to a race condition exploit when a user executes parallel curl requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;With SKILLS.md:&lt;/strong&gt; The agent's internal reasoning detects a state change trigger. It forces the SQL generation to include row-level isolation via &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; or requires a strict &lt;code&gt;Idempotency-Key&lt;/code&gt; header transaction check.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: User-Configured Webhooks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Without SKILLS.md:&lt;/strong&gt; The user prompts the AI to build an outbound webhook engine so users can get alerts. The AI uses a simple Axios/Fetch call passing the target parameter. An attacker signs up, sets their webhook to &lt;code&gt;[http://169.254.169.254/latest/meta-data/](http://169.254.169.254/latest/meta-data/)&lt;/code&gt;, and extracts cloud infrastructure IAM keys.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;With SKILLS.md:&lt;/strong&gt; The agent flags the user-controlled URL routing pattern. It refuses to output the code until it builds an accompanying domain allowlist check, wraps the execution in an isolated egress proxy, or isolates the protocol rules.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bigger Shift
&lt;/h2&gt;

&lt;p&gt;Today, engineers review AI-generated code. Tomorrow, AI systems will review AI-generated code. Eventually, entire engineering workflows will become completely autonomous.&lt;/p&gt;

&lt;p&gt;When that happens, security can no longer exist as an afterthought or a final manual compliance checklist performed at the tail end of a sprint. It has to become a core property of the AI's internal reasoning loop.&lt;/p&gt;

&lt;p&gt;AI does not automatically inherit security instincts. It inherits whatever mental models we explicitly give it. If you train an AI to think only like an engineer, it will build systems. If you train it to think like an attacker, it will help you build &lt;strong&gt;resilient&lt;/strong&gt; systems.&lt;/p&gt;

&lt;p&gt;The future belongs to the teams that can do both. Secure software is not created by accident; it is forged when someone spends enough time thinking about how it breaks first.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>Breaking to Build: How CTF and Bug Bounty Hunting Rewires System Design</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 31 May 2026 18:50:37 +0000</pubDate>
      <link>https://dev.to/shubham399/breaking-to-build-how-ctf-and-bug-bounty-hunting-rewires-system-design-2j7c</link>
      <guid>https://dev.to/shubham399/breaking-to-build-how-ctf-and-bug-bounty-hunting-rewires-system-design-2j7c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2F9824899b-3ed9-4d61-8e5a-d49568803653.png%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2F9824899b-3ed9-4d61-8e5a-d49568803653.png%2Fpublic" alt="Adversarial thinking for secure system design" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Software engineers are trained to create: read a requirement, design the happy path, write the code, pass tests, and ship. Authorized security practice adds a useful second question: &lt;strong&gt;what assumptions does this design make, and how could those assumptions fail?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Capture The Flag exercises and properly scoped vulnerability research are valuable because they train that question. The point is not to treat production systems as puzzles or to chase clever exploits. It is to bring adversarial thinking back into normal engineering work: define trust boundaries, verify authorization, reduce capability, log important decisions, and make unsafe behavior difficult by default.&lt;/p&gt;

&lt;p&gt;That change in perspective has improved how I design APIs, multi-tenant services, background workers, and operational tooling. These are the system-design lessons that stick.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Data has provenance, not automatic trust
&lt;/h2&gt;

&lt;p&gt;A database is durable storage, not a trust boundary. A value may have originated in a user form, webhook, import, support tool, partner API, or an earlier application bug. Reading it back from PostgreSQL does not make it safe for every new context.&lt;/p&gt;

&lt;p&gt;The key distinction is between a value’s &lt;strong&gt;provenance&lt;/strong&gt; and the &lt;strong&gt;sink&lt;/strong&gt; where it will be used. A display name may be valid as text in a JSON response but unsafe if inserted as HTML, used in a shell command, or interpolated into a query. The correct defense depends on that destination.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;untrusted input → validate shape at ingress → store as data
                                             ↓
                                 encode or parameterize at each sink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For HTML, prefer framework auto-escaping and avoid raw HTML rendering. If rich user-authored HTML is a genuine product requirement, sanitize it with a maintained, context-appropriate library and keep the allowed elements and attributes intentionally small. For SQL, use parameterized queries. For commands, avoid shell interpolation; use APIs that pass arguments separately whenever possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Good: the database driver keeps data separate from SQL syntax.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select id, name from projects where tenant_id = $1 and id = $2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Prefer UI/framework escaping for text. Avoid injecting user data as raw HTML.&lt;/span&gt;
&lt;span class="nf"&gt;renderText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“Sanitize everything” is too vague to be a reliable rule. Validate structure at ingress, preserve data as data, and encode or parameterize it for the specific output context. That approach also makes code review more precise: reviewers can ask, “what is the sink, and what protection matches it?”&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Design authorization into every object lookup
&lt;/h2&gt;

&lt;p&gt;Broken object-level authorization (often called IDOR or BOLA) is rarely fixed by choosing less guessable identifiers. UUIDs can reduce accidental enumeration, but they do not establish ownership or permission. The server must decide whether the authenticated principal may perform the requested action on the requested object.&lt;/p&gt;

&lt;p&gt;I prefer authorization-first repository methods over a generic “load by id” followed by an easily forgotten policy check. The method shape makes tenant and actor context mandatory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadProjectForRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;oneOrNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`select id, name, status
       from projects
      where id = $1
        and tenant_id = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadProjectForRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production policies are often more nuanced than tenant membership: roles, project membership, delegated access, ownership, resource state, and action type may all matter. Centralize that policy where possible, test both allow and deny cases, and return responses that do not reveal unnecessary details about resources outside the caller’s scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Turn ambient power into explicit capabilities
&lt;/h2&gt;

&lt;p&gt;Many high-impact failures begin with code that has more authority than it needs. A request handler that receives an all-powerful database client, a broad cloud credential, or a general-purpose administrative service can accidentally cross boundaries.&lt;/p&gt;

&lt;p&gt;Instead, expose narrow operations that express intent: &lt;code&gt;createInvoiceForTenant&lt;/code&gt;, &lt;code&gt;readOwnProfile&lt;/code&gt;, or &lt;code&gt;queueReportForProject&lt;/code&gt;. Pass an actor or capability explicitly. This makes authorization visible in APIs and reduces the chance that a helper silently acts with system-wide privileges.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RefundCapability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;maxAmountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefundCapability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amountCents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxAmountCents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForbiddenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Refund amount exceeds approval limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Scope the invoice lookup and record the decision before side effects.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amountCents&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a replacement for database permissions, network controls, or policy enforcement. It is an application-level design habit that makes least privilege easier to preserve.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Threat model before implementation details harden
&lt;/h2&gt;

&lt;p&gt;A short threat-modeling session early in a feature often prevents expensive rewrites later. It does not need a large ceremony. For a new endpoint or worker, I ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  What assets, actions, and data are worth protecting?&lt;/li&gt;
&lt;li&gt;  Who are the actors, and which identities or credentials can they present?&lt;/li&gt;
&lt;li&gt;  Where does untrusted data enter, cross a boundary, or reach a sensitive sink?&lt;/li&gt;
&lt;li&gt;  Which actions are irreversible, high-value, or easy to replay?&lt;/li&gt;
&lt;li&gt;  What evidence will help us detect and investigate misuse?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output can be a few bullets in the pull request. The value is forcing assumptions into the open before they become interfaces that every later service depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Secure defaults beat security checklists
&lt;/h2&gt;

&lt;p&gt;Security controls are more reliable when the safe path is the easiest path. Require tenant context in repository queries. Make internal fields opt-in in serializers. Set conservative timeouts. Deny access unless a policy grants it. Keep development diagnostics away from production responses.&lt;/p&gt;

&lt;p&gt;Good defaults also apply to sessions and credentials: short-lived tokens where practical, scoped service accounts, rotation procedures, explicit expiry, and revocation paths. A system should be able to remove access quickly without requiring a redesign during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Observability is a security control
&lt;/h2&gt;

&lt;p&gt;A policy decision that cannot be investigated is difficult to trust. Audit events should make important actions reconstructable: who initiated an operation, which resource and tenant were involved, which policy path allowed it, and whether the action succeeded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;project.read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allowed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs must be useful without becoming a second data leak. Avoid raw credentials, authentication headers, full payment details, and sensitive message bodies. Apply retention controls, restrict access to audit stores, and alert on meaningful patterns such as repeated authorization denials or unusual bursts of privileged actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test defenses in authorized environments
&lt;/h2&gt;

&lt;p&gt;Security testing belongs in environments where you have permission: local labs, CTFs, intentionally vulnerable training applications, staging systems, or programs with an explicit scope and safe-harbor policy. The goal is to validate defenses without creating risk for other people’s data or services.&lt;/p&gt;

&lt;p&gt;For product code, turn lessons into repeatable tests. Add authorization tests for cross-tenant access, property tests for parsers, integration tests for output encoding and serialization, and regression tests for issues already fixed. Treat a discovered weakness as an opportunity to improve a class of failures, not merely patch one endpoint.&lt;/p&gt;

&lt;p&gt;If you find a vulnerability in someone else’s system, follow the program’s reporting policy or use coordinated disclosure. Share the minimum evidence needed for reproduction, protect sensitive data, and give maintainers time to investigate and remediate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; Document data provenance and trust boundaries for new flows.&lt;/li&gt;
&lt;li&gt; Validate at ingress; encode or parameterize for each output sink.&lt;/li&gt;
&lt;li&gt; Scope every object lookup to the authenticated actor, tenant, and action.&lt;/li&gt;
&lt;li&gt; Expose narrow capabilities instead of ambient administrative access.&lt;/li&gt;
&lt;li&gt; Threat-model irreversible or high-value workflows before implementation.&lt;/li&gt;
&lt;li&gt; Use deny-by-default policies, short-lived credentials, and explicit revocation.&lt;/li&gt;
&lt;li&gt; Log security-relevant decisions without logging secrets or unnecessary personal data.&lt;/li&gt;
&lt;li&gt; Test authorization boundaries and regressions in authorized environments only.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Authorized offensive-security practice is valuable because it makes system design assumptions visible. It teaches that storage is not trust, identifiers are not authorization, and a working happy path is not the same as a safe system.&lt;/p&gt;

&lt;p&gt;The best outcome is not becoming suspicious of every line of code. It is building systems where boundaries, permissions, and recovery paths are explicit enough that ordinary engineering work stays secure by default.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ctf</category>
      <category>bugbounty</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Demystifying the Trinity: Functor, Applicative, and Monad in PureScript</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sat, 30 May 2026 04:24:48 +0000</pubDate>
      <link>https://dev.to/shubham399/demystifying-the-trinity-functor-applicative-and-monad-in-purescript-30m9</link>
      <guid>https://dev.to/shubham399/demystifying-the-trinity-functor-applicative-and-monad-in-purescript-30m9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fb545b1ef-a366-4f45-ae19-d8165269457d.png%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fb545b1ef-a366-4f45-ae19-d8165269457d.png%2Fpublic" alt="Functor, Applicative, and Monad in PureScript" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Functor, Applicative, and Monad can sound like a barrier to functional programming. They are not three competing ideas or magical containers. They are progressively more capable interfaces for working with a value while preserving the structure around it.&lt;/p&gt;

&lt;p&gt;In PureScript, that structure might represent an optional value (&lt;code&gt;Maybe a&lt;/code&gt;), a computation that can return an error (&lt;code&gt;Either e a&lt;/code&gt;), an array of possibilities, or an effectful program (&lt;code&gt;Effect a&lt;/code&gt;). The useful question is not “what metaphor fits?” but: &lt;strong&gt;what can I do without manually taking the value out and rebuilding its context?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the type, not the metaphor
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Maybe a&lt;/code&gt; says a value of type &lt;code&gt;a&lt;/code&gt; may be absent. &lt;code&gt;Either e a&lt;/code&gt; says a computation produces either an error &lt;code&gt;e&lt;/code&gt; or a successful value &lt;code&gt;a&lt;/code&gt;. &lt;code&gt;Effect a&lt;/code&gt; describes a program which, when run, may perform synchronous JavaScript effects and produce &lt;code&gt;a&lt;/code&gt;; asynchronous work is conventionally represented with &lt;code&gt;Aff a&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These types are not runtime security or validation by themselves. Data from HTTP, a database, or JavaScript is still untrusted until validated at the boundary. The abstractions below help us compose the resulting typed values cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functor: transform a value while keeping its context
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Functor&lt;/strong&gt; supports &lt;code&gt;map&lt;/code&gt; (also written &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt;). Given a normal function &lt;code&gt;a -&amp;gt; b&lt;/code&gt;, it transforms the value inside &lt;code&gt;f a&lt;/code&gt; into &lt;code&gt;f b&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Functor f where
  map :: forall a b. (a -&amp;gt; b) -&amp;gt; f a -&amp;gt; f b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;Maybe&lt;/code&gt;, mapping runs the function for &lt;code&gt;Just&lt;/code&gt; and preserves &lt;code&gt;Nothing&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module Main where

import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (logShow)

toCents :: Int -&amp;gt; Int
toCents dollars = dollars * 100

main :: Effect Unit
main = do
  let amount = Just 50 :: Maybe Int
  let missing = Nothing :: Maybe Int

  logShow (toCents &amp;lt;$&amp;gt; amount)  -- Just 5000
  logShow (toCents &amp;lt;$&amp;gt; missing) -- Nothing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toCents&lt;/code&gt; stays a simple &lt;code&gt;Int -&amp;gt; Int&lt;/code&gt; function. The &lt;code&gt;Maybe&lt;/code&gt; Functor owns the “what happens when the value is missing?” rule. Use a Functor whenever the next operation is a pure transformation of one available value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applicative: combine independent contextual values
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;Applicative&lt;/strong&gt; extends Functor with &lt;code&gt;pure&lt;/code&gt;, which places a value in a context, and &lt;code&gt;apply&lt;/code&gt; (written &lt;code&gt;&amp;lt;*&amp;gt;&lt;/code&gt;), which applies a contextual function to a contextual value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Functor f &amp;lt;= Applicative f where
  pure :: forall a. a -&amp;gt; f a
  apply :: forall a b. f (a -&amp;gt; b) -&amp;gt; f a -&amp;gt; f b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially readable when a constructor needs several independently obtained values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Prelude
import Data.Maybe (Maybe(..))

type User = { name :: String, id :: Int }

makeUser :: String -&amp;gt; Int -&amp;gt; User
makeUser name id = { name, id }

maybeName :: Maybe String
maybeName = Just "Alice"

maybeId :: Maybe Int
maybeId = Just 1024

maybeUser :: Maybe User
maybeUser = makeUser &amp;lt;$&amp;gt; maybeName &amp;lt;*&amp;gt; maybeId
-- Just { name: "Alice", id: 1024 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If either input is &lt;code&gt;Nothing&lt;/code&gt;, the result is &lt;code&gt;Nothing&lt;/code&gt;. Notice the function itself remains pure; Applicative handles the shared &lt;code&gt;Maybe&lt;/code&gt; context. &lt;code&gt;pure 42 :: Maybe Int&lt;/code&gt; produces &lt;code&gt;Just 42&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;“Independent” matters. Applicative composition fits when the next computation does not need the previous value to decide what computation to run. For validation that should collect several errors, use a validation type with an error-accumulating Applicative instance; &lt;code&gt;Either&lt;/code&gt; normally returns one failure rather than collecting all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monad: choose the next computation from the previous result
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Monad&lt;/strong&gt; adds &lt;code&gt;bind&lt;/code&gt; (often written &lt;code&gt;&amp;gt;&amp;gt;=&lt;/code&gt;). It lets the next function return another contextual value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Applicative m &amp;lt;= Monad m where
  bind :: forall a b. m a -&amp;gt; (a -&amp;gt; m b) -&amp;gt; m b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is needed when a plain &lt;code&gt;map&lt;/code&gt; would create nesting such as &lt;code&gt;Maybe (Maybe Int)&lt;/code&gt;, or when later work depends on an earlier successful result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Prelude
import Data.Either (Either(..))
import Data.Int (fromString)
import Data.Maybe (note)

parsePositiveInt :: String -&amp;gt; Either String Int
parsePositiveInt raw = do
  value &amp;lt;- note "Expected an integer" (fromString raw)
  if value &amp;gt; 0 then Right value
  else Left "Expected a positive integer"

orderTotal :: String -&amp;gt; String -&amp;gt; Either String Int
orderTotal rawPrice rawQuantity = do
  price &amp;lt;- parsePositiveInt rawPrice
  quantity &amp;lt;- parsePositiveInt rawQuantity
  pure (price * quantity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, each &lt;code&gt;&amp;lt;-&lt;/code&gt; extracts a successful &lt;code&gt;Right&lt;/code&gt; for the next line. If &lt;code&gt;parsePositiveInt&lt;/code&gt; returns &lt;code&gt;Left&lt;/code&gt;, the remaining work is skipped and that error becomes the result. That short-circuiting is behavior of the &lt;code&gt;Either&lt;/code&gt; Monad instance not a universal property of every Monad.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;do&lt;/code&gt; notation is readable &lt;code&gt;bind&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;PureScript’s &lt;code&gt;do&lt;/code&gt; notation makes sequential composition practical. Conceptually, this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;do
  price &amp;lt;- parsePositiveInt rawPrice
  quantity &amp;lt;- parsePositiveInt rawQuantity
  pure (price * quantity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is a readable form of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parsePositiveInt rawPrice &amp;gt;&amp;gt;= \price -&amp;gt;
  parsePositiveInt rawQuantity &amp;gt;&amp;gt;= \quantity -&amp;gt;
    pure (price * quantity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;do&lt;/code&gt; for effectful programs too. The same sequencing idea works for &lt;code&gt;Effect&lt;/code&gt;, &lt;code&gt;Aff&lt;/code&gt;, &lt;code&gt;Maybe&lt;/code&gt;, and &lt;code&gt;Either&lt;/code&gt;; only the meaning of sequencing changes with the instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical selection guide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Functor:&lt;/strong&gt; transform one value with a pure function. Example: format a &lt;code&gt;Maybe Date&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Applicative:&lt;/strong&gt; combine contextual values when their computations are independent. Example: construct a record from optional fields.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Monad:&lt;/strong&gt; continue with a computation selected by an earlier result. Example: parse an identifier, then load data using that identifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the least powerful abstraction that expresses the flow. &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt; is often clearer than &lt;code&gt;do&lt;/code&gt; for one transformation; &lt;code&gt;&amp;lt;*&amp;gt;&lt;/code&gt; clearly communicates independent inputs; use &lt;code&gt;do&lt;/code&gt; when sequencing or dependency is genuinely present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Calling every generic type constructor a “container.” It is a helpful first picture, not the definition.&lt;/li&gt;
&lt;li&gt;  Assuming types make untrusted input valid. Decode and validate runtime data before relying on static types.&lt;/li&gt;
&lt;li&gt;  Using Monad when Applicative expresses the intent better.&lt;/li&gt;
&lt;li&gt;  Assuming every Monad stops on failure. Arrays, state, readers, and effects have different sequencing behavior.&lt;/li&gt;
&lt;li&gt;  Confusing &lt;code&gt;Effect&lt;/code&gt; with asynchronous work. Use &lt;code&gt;Aff&lt;/code&gt; when the operation is asynchronous.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Functor, Applicative, and Monad are less mysterious when treated as composition tools. Functor transforms within a context. Applicative combines independent contextual values. Monad sequences computations whose next step depends on the previous result.&lt;/p&gt;

&lt;p&gt;Once these distinctions become familiar, types like &lt;code&gt;Maybe&lt;/code&gt;, &lt;code&gt;Either&lt;/code&gt;, &lt;code&gt;Effect&lt;/code&gt;, and &lt;code&gt;Aff&lt;/code&gt; stop feeling like ceremony. They become explicit descriptions of how values and computations should flow through the program.&lt;/p&gt;

</description>
      <category>functional</category>
      <category>programming</category>
      <category>typesystems</category>
      <category>purescript</category>
    </item>
    <item>
      <title>AI Is Making Senior Engineers 10x Faster - And 10x More Exhausted</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Thu, 28 May 2026 10:01:24 +0000</pubDate>
      <link>https://dev.to/shubham399/ai-is-making-senior-engineers-10x-faster-and-10x-more-exhausted-329e</link>
      <guid>https://dev.to/shubham399/ai-is-making-senior-engineers-10x-faster-and-10x-more-exhausted-329e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fc2aff1ec-2f4e-464d-b328-e8d2d6c00223.png%2Fpublic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F95a7ced4-fd82-4716-a6d0-b434f9e2b1f7%2Fc2aff1ec-2f4e-464d-b328-e8d2d6c00223.png%2Fpublic" alt="ai-senior" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When AI coding tools first appeared, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Nice. Less boilerplate.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now it sometimes feels like I’m managing a team of infinitely fast junior engineers that never sleep, occasionally hallucinate, and submit pull requests every 30 seconds.&lt;/p&gt;

&lt;p&gt;As a senior engineer, AI has made parts of my work much faster. It has also increased the amount of attention I have to spend deciding what is safe, useful, and worth keeping.&lt;/p&gt;

&lt;p&gt;Both things can be true at the same time.&lt;/p&gt;

&lt;p&gt;The “10x” in the title is rhetorical, not a measured productivity claim. Some tasks genuinely shrink from hours to minutes. Others simply move the effort: from typing code to framing the problem, checking assumptions, reviewing output, testing behavior, and owning the consequences after release.&lt;/p&gt;

&lt;p&gt;That distinction matters. Faster code generation is valuable. It is not the same thing as faster engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Good Part: AI Removes a Lot of Friction
&lt;/h2&gt;

&lt;p&gt;There is plenty to like. Used well, LLMs are helpful for repetitive and bounded work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;scaffolding a familiar endpoint or component&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;writing a first pass at tests and fixtures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;summarising an unfamiliar module before reading it closely&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;suggesting a refactor after the desired design is clear&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;drafting SQL, migrations, documentation, or scripts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;turning notes into a checklist, rollout plan, or incident update&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those jobs, AI can reduce blank-page time and context switching. It can make a prototype cheap enough to test. It can also give experienced engineers more room to focus on the parts that need judgment: product trade-offs, system boundaries, failure modes, and maintainability.&lt;/p&gt;

&lt;p&gt;That is real leverage. But leverage only helps when it is pointed in the right direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-Off: More Output Creates More to Validate
&lt;/h2&gt;

&lt;p&gt;The uncomfortable part is that AI changes the shape of senior work. It makes it easy to produce plausible code quickly, including code that is subtly wrong for a particular system.&lt;/p&gt;

&lt;p&gt;An assistant does not know the full history behind a strange-looking guard clause, the operational constraint hidden in an old incident report, or the business rule that lives outside the repository. It may infer those things correctly. It may also confidently invent an API, miss an authorization boundary, choose an unsafe default, or optimise the visible path while breaking an important edge case.&lt;/p&gt;

&lt;p&gt;That means the senior engineer is not just authoring less. They are acting as editor, reviewer, systems thinker, and accountable owner for a larger stream of generated output.&lt;/p&gt;

&lt;p&gt;The risk is not that every AI suggestion is bad. Many are perfectly serviceable. The risk is that clean, fluent code can create an illusion of understanding. Code that looks conventional is easier to accept before anyone has verified that it fits the domain.&lt;/p&gt;

&lt;p&gt;So the bottleneck often moves from production to validation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Does this satisfy the actual requirement, not just the prompt?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What assumptions did it make about data, permissions, latency, or failure?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does it fit existing conventions and architectural boundaries?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can the team explain, test, operate, and change it later?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are engineering questions. AI can assist with them, but it cannot take ownership of the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use AI Inside a Deliberate Review Workflow
&lt;/h2&gt;

&lt;p&gt;My most useful rule is simple: generated code should enter the same engineering workflow as any other change. “The model wrote it” is not a reason to lower the bar; if anything, it is a reason to make the path to production more explicit.&lt;/p&gt;

&lt;p&gt;A practical workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with the outcome and constraints.&lt;/strong&gt; Write down the user-facing behavior, non-goals, invariants, affected systems, and rollback plan before asking for an implementation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ask for a small change.&lt;/strong&gt; Prefer one function, one module, or one narrow slice over a large, multi-file rewrite. Smaller diffs are easier to understand and review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read the diff as the owner.&lt;/strong&gt; Do not review it like a copy editor. Trace important inputs and outputs, error paths, state changes, and permission checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the evidence.&lt;/strong&gt; Build, lint, type-check, test, and exercise the relevant behavior locally or in a safe environment. Add the tests that would have caught the failure you are worried about.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use human review for the meaningful change.&lt;/strong&gt; A second engineer is especially useful when the work crosses service boundaries, changes data, affects security, or introduces a new abstraction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Release with observability and a way back.&lt;/strong&gt; Feature flags, metrics, logs, alerts, and a rollback path turn uncertainty into something manageable.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not bureaucracy for its own sake. It is how a team prevents speed at the keyboard from becoming risk in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and Design Still Come First
&lt;/h2&gt;

&lt;p&gt;Good prompts help, but “context engineering” is more than writing a clever instruction. It is the work of making the problem legible: what the system does, where the boundaries are, what must not change, and how success will be observed.&lt;/p&gt;

&lt;p&gt;Before generating implementation code, I try to provide or decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the desired behavior and examples of valid and invalid cases&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the relevant interfaces, schemas, and repository conventions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;performance, privacy, security, and compatibility constraints&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;which dependencies are allowed and which patterns are off limits&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the tests, metrics, or acceptance criteria that define “done”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For complex work, it is often better to ask AI for options than for a finished patch. Ask it to identify trade-offs, list likely failure modes, or compare approaches against stated constraints. Then make the design decision deliberately. Once the design is clear, generation becomes much safer and faster.&lt;/p&gt;

&lt;p&gt;The senior skill is not producing the largest prompt or accepting the most output. It is reducing ambiguity before output exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality Guardrails Make Speed Safer
&lt;/h2&gt;

&lt;p&gt;Guardrails are not a substitute for judgment, but they reduce the chance that a rushed change becomes a costly surprise.&lt;/p&gt;

&lt;p&gt;At a minimum, teams should keep the ordinary safeguards healthy: formatting, linting, type checks, automated tests, dependency and secret scanning, code review, and CI that blocks obvious regressions. The exact stack will vary, but the principle is stable: make the safe path the easy path.&lt;/p&gt;

&lt;p&gt;AI-assisted work deserves a few additional habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep pull requests reviewable.&lt;/strong&gt; Split mechanical changes from behavioral changes. Avoid combining a broad refactor with a feature unless there is a compelling reason.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Require ownership of every line.&lt;/strong&gt; The author should be able to explain why the code exists, what it assumes, and how it fails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat generated dependencies and snippets with caution.&lt;/strong&gt; Verify package names, licenses, versions, APIs, and security implications rather than trusting a suggestion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test boundaries, not only happy paths.&lt;/strong&gt; Focus on authorization, concurrency, retries, partial failure, malformed input, migration safety, and backward compatibility where relevant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protect sensitive context.&lt;/strong&gt; Follow company policy for source code, credentials, customer data, and internal documents. A convenient prompt should not become a data-handling mistake.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These practices help regardless of who wrote the first draft. AI just makes their absence easier to notice because it can create so much change so quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Changes Team and Process, Not Just Individual Productivity
&lt;/h2&gt;

&lt;p&gt;The biggest effects are often social. If one person can generate a large patch in an afternoon, review queues can fill faster than reviewers can reason about them. If every task is treated as instant because a tool exists, planning becomes disconnected from integration, verification, and operational work.&lt;/p&gt;

&lt;p&gt;Teams should therefore measure more than throughput. Deployment confidence, escaped defects, incident load, lead time through review, on-call pain, maintainability, and the ability of more than one person to work on a system all matter. A higher count of merged lines or tickets is not automatically progress.&lt;/p&gt;

&lt;p&gt;There is also a learning concern. Junior engineers still need feedback loops that teach them to reason about systems, debug failures, and make trade-offs. Giving them generated solutions without discussion may accelerate a task while weakening the path to independent judgment. Pairing, design reviews, and asking people to explain a proposed change are more valuable, not less, when code is cheap to produce.&lt;/p&gt;

&lt;p&gt;For senior engineers, the role increasingly includes setting norms: when AI is appropriate, what must be disclosed in a pull request, which checks are mandatory, and when a design conversation should happen before generation. Clear expectations reduce both risk and resentment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sustainable AI Use Means Protecting Attention
&lt;/h2&gt;

&lt;p&gt;The exhaustion is not caused only by more work. It comes from rapid switching: prompt, inspect, correct, test, prompt again, answer review comments, and repeat. Constantly evaluating plausible output can be mentally expensive, especially when the code touches unfamiliar parts of a system.&lt;/p&gt;

&lt;p&gt;To make AI useful without letting it consume the day, I try to use it intentionally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Batch exploratory prompting instead of interrupting every few minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reserve uninterrupted time for design, deep reading, and difficult debugging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stop generating when I cannot clearly articulate what I am asking for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prefer a small, understood solution over a large “complete” patch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use checklists for repeatable review work so attention is saved for the novel risks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Leave a decision record when the reasoning will matter to the next person.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the fastest move is to write the small piece of code yourself. Sometimes it is to ask AI for a test matrix or a second opinion. Sustainable use is not maximising prompts. It is choosing the tool mode that preserves clarity and energy for the work only humans on the team can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Leverage Is Judgment
&lt;/h2&gt;

&lt;p&gt;AI is making some parts of senior engineering faster, sometimes dramatically. It is also making judgment, review, context, and operational responsibility more visible.&lt;/p&gt;

&lt;p&gt;That does not make the tools a mistake. It makes the surrounding discipline more important. Use AI to remove friction, accelerate exploration, and handle bounded work. Keep humans responsible for design, validation, trade-offs, and the health of the system over time.&lt;/p&gt;

&lt;p&gt;The goal is not to generate code as fast as possible. The goal is to ship changes the team understands, can support, and can improve without burning out the people accountable for them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>career</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
