<?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>handoff: Give the Next AI Agent the Context It Actually Needs</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 06 Sep 2026 01:14:21 +0000</pubDate>
      <link>https://dev.to/shubham399/handoff-give-the-next-ai-agent-the-context-it-actually-needs-pnb</link>
      <guid>https://dev.to/shubham399/handoff-give-the-next-ai-agent-the-context-it-actually-needs-pnb</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%2F80e9b422-8d71-42ef-ae27-6864b292a5b2%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%2F80e9b422-8d71-42ef-ae27-6864b292a5b2%2Fpublic" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI-assisted engineering does not usually fail because an agent forgets everything. It fails because a new session receives the wrong kind of context. Give it too little and it repeats discovery, reopens decisions, or changes the wrong files. Give it an entire transcript and it has to untangle settled facts, abandoned branches, tool noise, and current work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-handoff" rel="noopener noreferrer"&gt;handoff&lt;/a&gt; is a skill for the seam between sessions. Invoked with &lt;code&gt;/handoff&lt;/code&gt;, it compacts the active conversation into a handoff document that a fresh agent can read to continue the work. Its design is intentionally selective: it carries the live thread, references durable artifacts instead of copying them, saves the document to the operating system’s temporary directory rather than the workspace, includes suggested skills, and redacts sensitive material.&lt;/p&gt;

&lt;p&gt;The central idea is compaction. Preserve momentum, not every word.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why session changes are a real engineering boundary
&lt;/h2&gt;

&lt;p&gt;A context switch happens more often than teams admit. A long thread approaches a limit. Someone stops work for the day. A specialist agent completes research and another agent implements. A human changes the task’s priority. A review uncovers a concern that needs a new conversation.&lt;/p&gt;

&lt;p&gt;Without an explicit handoff, the next participant must reconstruct intent from code and chat history. Code tells you what exists, not always why it was chosen. A ticket may state the desired outcome but not the current blocker. A plan may be accurate but not reveal which task was half-finished when the session ended. The handoff fills that narrow but valuable gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in a high-quality handoff
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The live thread
&lt;/h3&gt;

&lt;p&gt;State what is in flight in plain language. “Implementing organization invitations” is too broad. “Persistence and service-layer authorization are complete; email delivery remains unimplemented because the existing notification abstraction does not support signed links” is useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters now
&lt;/h3&gt;

&lt;p&gt;Explain the immediate context: a test is failing, an API decision is pending, a migration must be reviewed, or a release is blocked. This helps the next agent choose the right first action rather than simply continuing the last visible edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settled decisions
&lt;/h3&gt;

&lt;p&gt;List only decisions the next session must not accidentally undo. Include the rationale briefly and point to the specification, ADR, issue, or code path that is authoritative. For example: “Invites expire after seven days; accepted in ADR-014. Do not change token model without product approval.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Open questions and blockers
&lt;/h3&gt;

&lt;p&gt;Be concrete about what is unresolved, who owns the choice, and what evidence is needed. “Need to decide email provider” is weak. “Choose whether to send through the existing asynchronous notification service or add a transactional provider; inspect incident history and delivery requirements first” gives the next agent a route forward.&lt;/p&gt;

&lt;h3&gt;
  
  
  References, not duplicates
&lt;/h3&gt;

&lt;p&gt;handoff specifically avoids restating material already captured in a spec, plan, ADR, issue, commit, or diff. It references a path or URL instead. That is a safeguard against documentation drift. If a spec changes, the project has one source of truth rather than five copied versions in old handoff notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why temporary storage matters
&lt;/h2&gt;

&lt;p&gt;The skill saves handoff documents to the OS temporary directory, not the repository workspace. This is a useful design decision. A handoff is session glue, not necessarily a product artifact that should be committed, maintained, and discovered by every future developer.&lt;/p&gt;

&lt;p&gt;That does not mean a handoff is disposable in the everyday sense. It may contain crucial information for the next session. It means it should not become another competing project document. Durable decisions belong in durable artifacts; the handoff points to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to invoke it well
&lt;/h2&gt;

&lt;p&gt;handoff is user-invoked rather than automatic. That is good: humans know when a transition is intentional. Pass a note describing what the next session is for so the document can be tailored to the continuation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/handoff Next session will implement the API routes and integration tests for invitation acceptance. Preserve the settled authorization rules, current migration state, failing test details, and links to the approved spec and diff.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A targeted instruction leads to a targeted note. “Summarize everything” tends to reproduce the problem handoff is designed to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  A usable handoff template
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Objective
- What outcome are we delivering?

Current state
- What is complete? What is partially complete?

Settled decisions
- Decision, rationale, authoritative reference.

Open questions / blockers
- What is unresolved, who owns it, what evidence is needed?

Validation
- Commands run, test results, environment assumptions.

References
- Specs, plans, ADRs, issues, commits, diffs, relevant files.

Next action
- The first concrete thing the next agent should do.

Suggested skills
- Which skill or workflow should be used next?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template is deliberately operational. It gives a successor enough to act without pretending to be a full project history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: weak versus strong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Weak:&lt;/strong&gt; “Worked on auth. Some tests pass. Continue tomorrow.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong:&lt;/strong&gt; “Goal: add passwordless sign-in for existing users. Completed token persistence and expiry tests; API endpoint exists but is not wired to mail delivery. Decision: do not reveal whether an email belongs to an account see security section in &lt;code&gt;docs/auth-spec.md&lt;/code&gt;. Blocker: current notification adapter cannot send template variables. Next: inspect adapter contract and propose the smallest compatible extension. Ran &lt;code&gt;bun test auth&lt;/code&gt;: 18 pass, 2 skipped. Suggested skills: domain-model if adapter change affects notification domain; TDD for implementation.”&lt;/p&gt;

&lt;p&gt;The strong version contains less total history but far more usable context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and privacy
&lt;/h2&gt;

&lt;p&gt;handoff is designed to redact secrets, passwords, API keys, and personally identifiable information. Do not treat automatic redaction as permission to paste secrets into a conversation. The safer practice is to reference secret-management locations and use neutral identifiers: “Production API credential is configured in the deployment secret store,” not the credential itself.&lt;/p&gt;

&lt;p&gt;Also consider sensitive business context. A handoff can reveal incident details, customer behavior, or internal strategy. Keep it scoped to the recipient and store durable security findings in the appropriate controlled artifact.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Turning the handoff into a transcript
&lt;/h3&gt;

&lt;p&gt;Long chronological summaries force the next agent to reread history. Prefer current state, decisions, references, and next actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failing to name the next action
&lt;/h3&gt;

&lt;p&gt;A handoff that says “continue implementation” leaves the successor to choose a starting point. State the first file to inspect, command to run, or decision to obtain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicating the spec
&lt;/h3&gt;

&lt;p&gt;Copying a plan makes it stale. Reference the canonical version and record only what changed in the live conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hiding uncertainty
&lt;/h3&gt;

&lt;p&gt;Say when a conclusion is tentative. A false sense of certainty is more damaging than an explicit unresolved question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where handoff fits in an agent workflow
&lt;/h2&gt;

&lt;p&gt;Use a planning skill to clarify a feature, a structured build workflow to implement it, concise execution mode when appropriate, and handoff at the transition point. Handoff is not a replacement for specs or commits. It is the bridge that makes those artifacts usable in the next context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;A good handoff lets a new agent inherit the work’s momentum without inheriting its noise. Capture the live thread, link the stable artifacts, make uncertainty visible, and state the next action. That is enough context to continue well and not so much that the next session starts by getting lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-handoff" rel="noopener noreferrer"&gt;AI Hero: handoff&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/mattpocock/skills/tree/main/skills/productivity/handoff" rel="noopener noreferrer"&gt;mattpocock/skills: handoff&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Caveman Ultra Mode: Make Your Coding Agent Say Less, Not Think Less</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 30 Aug 2026 09:17:59 +0000</pubDate>
      <link>https://dev.to/shubham399/caveman-ultra-mode-make-your-coding-agent-say-less-not-think-less-pmf</link>
      <guid>https://dev.to/shubham399/caveman-ultra-mode-make-your-coding-agent-say-less-not-think-less-pmf</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%2F05d26c94-207c-4120-b3d3-8122234b1e4f%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%2F05d26c94-207c-4120-b3d3-8122234b1e4f%2Fpublic"&gt;&lt;/a&gt;AI coding agents have a communication problem. They can be technically capable and still make a development loop feel slow because every small action arrives with a paragraph of ceremony: a restatement of the ticket, a promise to investigate, an explanation of an obvious command, and a summary that repeats the first three things.&lt;/p&gt;

&lt;p&gt;That style is sometimes useful. It is not useful all the time. When you are deep in a known codebase and want to diagnose a failing test, inspect a diff, or make a narrow fix, the value is usually in four things: what the agent found, what it changed, how it verified the change, and what remains uncertain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/juliusbrussee/caveman" rel="noopener noreferrer"&gt;Caveman&lt;/a&gt; is a skill/plugin built around that distinction. It makes a coding agent communicate in short, direct language its deliberately rough “caveman-speak” while aiming to leave code, commands, and errors byte-for-byte intact. The project describes this as making the agent’s mouth smaller rather than its brain smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ultra Mode is an interface choice
&lt;/h2&gt;

&lt;p&gt;The useful way to understand Caveman is not as a substitute for reasoning. It is an interface choice for the execution phase of work. A terse agent should still inspect the repository, follow the test suite, notice ambiguity, and say when evidence is missing. It simply should not pad a simple finding with social filler.&lt;/p&gt;

&lt;p&gt;Compare these two reports:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I’ve taken a look at the component and the reason it is re-rendering is likely because a new object reference is created during each render cycle. I recommend using useMemo to memoize that object.”&lt;/p&gt;

&lt;p&gt;“New object ref each render. Inline prop = new ref = re-render. Wrap in &lt;code&gt;useMemo&lt;/code&gt;.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second version is not appropriate for a design document. For a developer actively debugging a React component, however, it is easier to scan and easier to act on. The underlying technical claim is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Caveman actually promises
&lt;/h2&gt;

&lt;p&gt;The Caveman repository says it works with Claude Code, Codex, Gemini, Cursor, Windsurf, Cline, Copilot, and other agent environments. It claims “65% fewer output tokens” and emphasizes that the saving applies to output, not input context. Treat that figure as the project’s own claim, not as an independent benchmark or a guarantee for every model and task.&lt;/p&gt;

&lt;p&gt;That caveat matters. Token savings vary with the agent, the task, the amount of tool output, and the prompt. Still, the basic mechanism is sound: if an agent stops producing repetitive prose, it emits fewer tokens and consumes less attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where concise mode earns its place
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Known-scope debugging
&lt;/h3&gt;

&lt;p&gt;Suppose CI says an authentication test has failed. You want the agent to locate the relevant middleware, compare the expiry condition with the test, fix the defect, and report the result. You do not need a tutorial on authentication on every turn.&lt;/p&gt;

&lt;p&gt;A useful compact report is: “Expiry check uses &lt;code&gt;&amp;lt;=&lt;/code&gt;; test expects boundary valid. Changed to &lt;code&gt;&amp;lt;&lt;/code&gt;. Target tests pass. Risk: confirm API contract for exact expiry instant.” It retains the evidence and the unresolved risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small implementation loops
&lt;/h3&gt;

&lt;p&gt;During a tightly defined change, concise updates keep the human in the loop. Ask for a plan once, approve it, then request short progress reports. The agent can say which files changed, which tests ran, and whether it needs a decision. This is especially useful when you are reviewing diffs alongside the agent rather than delegating an entire feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terminal-heavy work
&lt;/h3&gt;

&lt;p&gt;Commands, build output, migration results, and test failures already contain a lot of text. Adding a long prose explanation around every command makes the signal harder to find. Caveman’s stated rule of leaving commands and errors unchanged is important here: compress the commentary, not the evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code review follow-ups
&lt;/h3&gt;

&lt;p&gt;After a review comment, the desired loop is often mechanical: inspect, decide whether the comment is valid, make a focused change, run validation. Terse reports help reviewers see the outcome without re-reading an essay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Ultra Mode is a bad fit
&lt;/h2&gt;

&lt;p&gt;Concise output can conceal uncertainty. Do not enable it blindly for the parts of engineering where nuance is the work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Discovery and requirements:&lt;/strong&gt; assumptions need to be stated, not compressed away.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Architecture:&lt;/strong&gt; alternatives, constraints, and trade-offs deserve explicit comparison.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security and privacy:&lt;/strong&gt; threat models and residual risk require careful language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Incident response:&lt;/strong&gt; timelines, impact, and confidence levels must be precise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Onboarding and teaching:&lt;/strong&gt; the explanation is part of the deliverable.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure mode is not that short answers are inherently shallow. The failure mode is mistaking a short answer for a complete decision record.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to preserve accountability
&lt;/h2&gt;

&lt;p&gt;Use a compact reporting contract. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Work in concise mode.
Before edits: inspect relevant files and state the likely cause in 1–3 bullets.
After edits: list files changed, validation run, result, and remaining risk.
Ask before making an irreversible or scope-expanding decision.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the agent permission to be brief while defining the minimum evidence you expect. “Concise” should never mean “silent about a failed test” or “quiet about an assumption.”&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical operating model
&lt;/h2&gt;

&lt;p&gt;Split agent work into two modes. In &lt;strong&gt;decision mode&lt;/strong&gt;, ask the agent to explore, explain, compare options, and surface uncertainty. In &lt;strong&gt;execution mode&lt;/strong&gt;, once the approach is approved, use Caveman-style reporting to keep the loop fast.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Frame the task and constraints in normal language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask for a short plan and acceptance checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Approve or revise the approach.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Switch to Ultra Mode for implementation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Require exact tests, commands, and a final risk note.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Return to normal mode for a postmortem or architectural follow-up.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This avoids the false choice between a verbose agent and a reckless one. You can have a careful process and a compact execution interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and activation
&lt;/h2&gt;

&lt;p&gt;The project documents broad agent support and provides install instructions for macOS, Linux, WSL, Git Bash, and Windows PowerShell. Its README says Caveman can be turned on with &lt;code&gt;/caveman&lt;/code&gt; or a request such as “talk like caveman,” and turned off with “normal mode.” Always use the repository’s current instructions for your particular agent harness; plugin conventions change quickly.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Using it before the problem is framed
&lt;/h3&gt;

&lt;p&gt;If your request is “make our billing system better,” terse mode will not make the request less ambiguous. First establish scope, owners, constraints, and measures of success.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measuring only token count
&lt;/h3&gt;

&lt;p&gt;A lower token count is useful only if it does not create more clarification rounds, missed constraints, or review burden. Measure cycle time and defect rate, not just output length.&lt;/p&gt;

&lt;h3&gt;
  
  
  Suppressing useful warnings
&lt;/h3&gt;

&lt;p&gt;Tell the agent that blockers and risks must remain explicit. A one-line warning is better than no warning; a short report is not permission to omit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Caveman Ultra Mode is most valuable when verbosity is the bottleneck and the work is already understood. It can reduce output overhead and make development status easier to scan. It cannot replace design, testing, or engineering judgment. Use it to make execution quieter not to make important thinking disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/juliusbrussee/caveman" rel="noopener noreferrer"&gt;Caveman repository and README&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Superpowers for Coding Agents: Turn Vague Requests Into Tested Changes</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 23 Aug 2026 11:55:36 +0000</pubDate>
      <link>https://dev.to/shubham399/superpowers-for-coding-agents-turn-vague-requests-into-tested-changes-2h2g</link>
      <guid>https://dev.to/shubham399/superpowers-for-coding-agents-turn-vague-requests-into-tested-changes-2h2g</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%2F1936ebba-e3c9-431c-ba9b-a9ac0bc1bf87%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%2F1936ebba-e3c9-431c-ba9b-a9ac0bc1bf87%2Fpublic" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fastest way for an AI coding agent to create expensive work is to start coding too soon. A request arrives, the agent infers the missing requirements, selects an architecture, edits several files, and produces a plausible patch. The patch may compile. It may even pass a narrow test. But it can still solve the wrong problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt; is a methodology and a set of composable skills intended to change that default. Rather than treating code generation as the first step, it asks the agent to clarify the outcome, develop and get approval for a design, make an implementation plan, use true red/green test-driven development, carry out the work in small tasks, and review what was built.&lt;/p&gt;

&lt;p&gt;That is not bureaucracy for its own sake. It is a response to the fact that AI agents are very good at filling gaps with plausible assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: gates before generation
&lt;/h2&gt;

&lt;p&gt;Superpowers describes a coding-agent workflow that begins when the agent recognizes it is building something. Instead of jumping into implementation, it steps back and asks what the user is really trying to accomplish. It then develops a specification, presents the design in readable chunks, and waits for sign-off before producing a detailed plan.&lt;/p&gt;

&lt;p&gt;Only after approval does the workflow move to implementation. The repository emphasizes red/green TDD, YAGNI build only what is needed and DRY, avoiding needless duplication. It also describes subagent-driven development for executing and reviewing tasks.&lt;/p&gt;

&lt;p&gt;Each gate exists because a different kind of failure is cheaper to catch early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Clarification catches misunderstood outcomes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Design review catches wrong shapes and missing constraints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Planning catches hidden dependencies and sequencing errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tests catch behavioral regressions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Review catches mismatches between the intended change and the actual diff.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why vague requests are dangerous
&lt;/h2&gt;

&lt;p&gt;Consider the request: “Add team invitations.” That sounds actionable, but it leaves a large number of product and system decisions unanswered. Are invites email-based? Does an invitation expire? Can it be revoked? What role does an invited person receive? What happens if they already have an account? What happens if they belong to another organization? Is the action auditable? Can an administrator resend an invitation? Which failure states are visible to users?&lt;/p&gt;

&lt;p&gt;A coding agent that immediately creates an &lt;code&gt;invitations&lt;/code&gt; table has already made decisions. A structured workflow makes those decisions visible before they become database migrations, endpoints, and UI states that are expensive to unwind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase one: establish the outcome
&lt;/h2&gt;

&lt;p&gt;Good clarification focuses on observable success, constraints, and non-goals. Ask for the user story, affected actors, existing behavior that must remain stable, and the acceptance checks that will show the task is complete.&lt;/p&gt;

&lt;p&gt;For the invitation example, a useful outcome might be: “Organization owners can invite an email address to a workspace. Invites expire after seven days, can be revoked, grant a selected role on acceptance, and must not expose workspace existence to unauthenticated users.” That is already more useful than “add invitations” because it gives design and tests a target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase two: agree on a design
&lt;/h2&gt;

&lt;p&gt;The Superpowers README describes showing design material in chunks small enough for a human to read and approve. That is a practical design principle for agent collaboration. A giant wall of architecture text is difficult to review; a sequence of focused decisions is easier to challenge.&lt;/p&gt;

&lt;p&gt;A design should cover the parts most likely to create irreversible work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Domain entities and ownership boundaries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API contracts and error behavior&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authorization checks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State transitions and lifecycle rules&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data migration and backward compatibility&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Observability and operational failure modes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Explicit non-goals&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Approval is not a ceremony. It is the moment a human says, “Yes, this is the product behavior we mean to own.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase three: write an implementable plan
&lt;/h2&gt;

&lt;p&gt;After design sign-off, the plan should break work into small, ordered tasks. Each task should identify the relevant files or components, the change, and the validation method. The plan should be specific enough that another competent developer or another agent can execute it without reinterpreting the original request.&lt;/p&gt;

&lt;p&gt;For example, an invitation feature might be sliced into: define domain rules and tests; add persistence and migration; add service-layer authorization; expose API routes; implement email delivery abstraction; add UI states; add integration tests; document rollout and monitoring. The exact order depends on the codebase, but the principle is stable: build vertical, verifiable increments.&lt;/p&gt;

&lt;h2&gt;
  
  
  True red/green TDD with an agent
&lt;/h2&gt;

&lt;p&gt;Superpowers emphasizes true red/green TDD. The important distinction is temporal: tests should express expected behavior before implementation is accepted. Writing tests after code can still be useful, but it often records what the implementation happened to do instead of what the system should do.&lt;/p&gt;

&lt;p&gt;For an invitation flow, a red test might assert that a revoked invite cannot be accepted, that an expired invite produces a specific error, or that a non-owner cannot create an invite. The implementation then moves the tests to green. This makes behavior reviewable in a form more durable than conversational claims.&lt;/p&gt;

&lt;p&gt;TDD does not guarantee a correct product decision. It guarantees that selected behavior is checked. That is why clarification and design must precede it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subagents and review
&lt;/h2&gt;

&lt;p&gt;The methodology describes subagent-driven development: agents can work through specific tasks, inspect work, and review outcomes. Parallelism is useful only when task boundaries are real. Separate agents may investigate independent modules or review a completed change, but two agents editing the same design decision without coordination can multiply confusion.&lt;/p&gt;

&lt;p&gt;Use subagents to create independent evidence: one investigates existing conventions, one drafts tests, one reviews a diff against the approved design. Keep a single source of truth for decisions and require the final reviewer to compare the result against that source.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use Superpowers
&lt;/h2&gt;

&lt;p&gt;This approach is valuable for new features, multi-file refactors, regressions with unclear causes, public API changes, and tasks where a mistaken assumption creates real rework. It is deliberately not the fastest method for changing a typo, renaming a local variable, or making a clearly scoped configuration edit.&lt;/p&gt;

&lt;p&gt;The question is not “Is this task large?” Ask instead: “What does it cost if the agent chooses the wrong interpretation?” If the answer is high, the structure pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adopting the workflow without overdoing it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Start with one medium-risk feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Require a written success statement and non-goals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Review the design before code changes begin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Require a task plan with tests attached to each behavioral slice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Review the final diff against the approved design, not only against the tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Record what produced rework and tune the workflow.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Teams should not judge the method by how polished the agent’s plan sounds. Judge it by concrete outcomes: fewer late requirement changes, clearer diffs, more reliable tests, and less rediscovery during review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Superpowers does not replace
&lt;/h2&gt;

&lt;p&gt;No agent workflow can replace product ownership, security review, or accountability for production changes. A well-structured plan can still optimize the wrong metric. A green suite can still omit a real-world edge case. Treat the framework as a way to make decisions and evidence visible not as a guarantee of correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Superpowers is useful because it gives an eager coding agent permission to pause. It turns a vague request into an agreed design, a plan, tested behavior, and a reviewable change. For meaningful engineering work, that pause is often the fastest part of the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;obra/superpowers README&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Use grill-me to Pressure-Test an AI Implementation Plan Before Code</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sat, 15 Aug 2026 23:45:03 +0000</pubDate>
      <link>https://dev.to/shubham399/use-grill-me-to-pressure-test-an-ai-implementation-plan-before-code-11e6</link>
      <guid>https://dev.to/shubham399/use-grill-me-to-pressure-test-an-ai-implementation-plan-before-code-11e6</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%2F6019e935-fa36-47a3-a057-b164bbb44afe%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%2F6019e935-fa36-47a3-a057-b164bbb44afe%2Fpublic" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many software mistakes begin as decisions that nobody explicitly made. A feature request sounds clear enough, an AI coding agent begins implementation, and the details get settled by whichever model output appears first. Later, the team discovers that “add roles,” “cache this endpoint,” or “support collaboration” contained several linked product, data, security, and rollout choices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-grill-me" rel="noopener noreferrer"&gt;grill-me&lt;/a&gt; is designed for the moment before that happens. It is a manually invoked skill that interviews you about a plan or design until the important decision tree is resolved. It asks one question at a time, supplies a recommended answer, and waits for feedback before continuing. Crucially, if an answer can be discovered by inspecting the codebase, the agent should investigate rather than ask you to recreate repository facts from memory.&lt;/p&gt;

&lt;p&gt;The name is playful. The underlying practice is serious: force a proposal to survive questions before code makes it expensive to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What grill-me is and is not
&lt;/h2&gt;

&lt;p&gt;grill-me is not a general implementation workflow. It does not replace a specification, an issue breakdown, tests, or code review. It is a pressure-test for a direction that is not yet sufficiently settled to build.&lt;/p&gt;

&lt;p&gt;That narrowness is its strength. Teams often need an agent to push back, not agree. A conventional assistant can be biased toward accepting the first plausible framing. A grilling workflow instead asks what would make the plan fail: what is ambiguous, what constraint is missing, what decision is being deferred accidentally, and what evidence should be checked in the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one question at a time matters
&lt;/h2&gt;

&lt;p&gt;A long questionnaire feels efficient but usually is not. It asks about details before the premise is fixed, and the answers become inconsistent. A sequential interview allows dependencies to resolve in order.&lt;/p&gt;

&lt;p&gt;For example, consider “add organization-level roles.” The first question might be whether roles are global or scoped to each organization. That answer determines whether membership is a separate domain entity. The next question may be whether permissions are static role bundles or configurable. Only then does it make sense to ask about API shape, migration, administration UI, or audit requirements.&lt;/p&gt;

&lt;p&gt;By proceeding one decision at a time, the skill turns a vague feature into a chain of explicit commitments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a proposal, not a blank page
&lt;/h2&gt;

&lt;p&gt;The best grilling sessions begin with enough context to challenge. State the desired outcome, the affected users, known constraints, existing artifacts, and the decision you want help making. Do not ask “what should we build?” when you really mean “which of two approaches fits this codebase?”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Grill this proposal before implementation.
Goal: organization owners can invite members by email.
Constraints: existing RBAC model, no public workspace enumeration,
and invitations must be revocable.
Inspect the relevant auth and membership code first.
Ask one question at a time. Recommend an answer, explain the trade-off,
and record settled decisions and open risks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the agent a job: interrogate the plan using evidence, not invent the product from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions worth asking before code
&lt;/h2&gt;

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

&lt;p&gt;What user behavior changes? Who can trigger it? Who can observe it? What explicitly remains out of scope? Scope questions prevent a small feature from quietly becoming a platform redesign.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain model
&lt;/h3&gt;

&lt;p&gt;What entities exist? Who owns them? What states can they occupy? What transitions are allowed? For invitations: pending, accepted, revoked, expired, perhaps resent. Decide whether resending creates a new token, extends expiry, or simply reuses a record. These are product rules with storage consequences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization
&lt;/h3&gt;

&lt;p&gt;Who may create, view, revoke, accept, or resend the resource? Are permissions checked at an organization boundary, a project boundary, or both? What information should a denial reveal? Authorization is not a final middleware detail; it belongs in the plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure and recovery
&lt;/h3&gt;

&lt;p&gt;What happens when an email is already associated with a member? When delivery fails? When two administrators act simultaneously? When a client retries? A plan that only describes the happy path is still incomplete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compatibility and rollout
&lt;/h3&gt;

&lt;p&gt;Will the change alter stored data, public APIs, permissions, or client assumptions? How will it be migrated? How will you monitor behavior after release? The right answer may be “no special rollout needed,” but it should be consciously chosen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use repository evidence
&lt;/h2&gt;

&lt;p&gt;One of grill-me’s best rules is that codebase facts should be discovered, not delegated to the user’s memory. Before asking whether an endpoint follows REST or RPC conventions, inspect existing endpoints. Before proposing a new role table, inspect the existing authorization system. Before recommending a migration, inspect database conventions and deployment tooling.&lt;/p&gt;

&lt;p&gt;This does not mean the codebase gets the final word. Existing patterns can be wrong or outdated. It means the agent should distinguish “we should deviate deliberately” from “we did not know the convention existed.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What a successful session produces
&lt;/h2&gt;

&lt;p&gt;The output should be more than a transcript. It should leave behind a concise decision record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The confirmed outcome and non-goals&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Key decisions and their rationale&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alternatives considered and rejected&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Assumptions that need validation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open risks or owners&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The recommended next artifact: domain model, PRD, spec, issues, or implementation plan&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives a later agent something durable to follow. It also makes review better because reviewers can see what the code is intended to embody.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where grill-me fits now
&lt;/h2&gt;

&lt;p&gt;AI Hero notes an important update from Matt Pocock: for coding-oriented planning, he now generally recommends &lt;strong&gt;domain-model&lt;/strong&gt; as the starting point when you want to align a plan with codebase language, &lt;code&gt;CONTEXT.md&lt;/code&gt;, and architecture decision records. The suggested broader sequence is &lt;code&gt;domain-model → to-prd → to-issues → tdd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This does not make grill-me obsolete. It gives it a clearer role. Use grill-me as a lighter, targeted pressure-test when you have a plan that needs interrogation. Use a broader domain-model workflow when the task needs deep alignment with the application’s vocabulary and existing decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Asking hypothetical questions forever
&lt;/h3&gt;

&lt;p&gt;Questions should converge toward a decision. If a scenario has little likelihood or impact, note it as a risk rather than letting it block every other choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Letting recommendations become decisions
&lt;/h3&gt;

&lt;p&gt;The agent can recommend; the responsible human should approve. Record when a recommendation is accepted, changed, or deferred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skipping evidence gathering
&lt;/h3&gt;

&lt;p&gt;A beautiful interview based on false assumptions is still waste. Require inspection of relevant code, documentation, and recent diffs first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calling it planning when no artifact remains
&lt;/h3&gt;

&lt;p&gt;Capture the conclusions. Otherwise the next implementation session will reopen the same questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable ritual
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Write a one-paragraph proposal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Link relevant files, tickets, ADRs, and constraints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask the agent to inspect before questioning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run one-question-at-a-time grilling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Record decisions, non-goals, and unresolved risks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn the result into the next formal artifact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Only then begin implementation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The time spent here is not delay. It is a way to move uncertainty from code into conversation, where it is cheaper to resolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Use grill-me when a feature direction feels plausible but has not earned implementation yet. It is particularly valuable when several decisions depend on each other and you want an agent to challenge you rather than cheerlead. Code should be the result of a decision, not the place where the decision first becomes visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-grill-me" rel="noopener noreferrer"&gt;AI Hero: grill-me&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/mattpocock/skills/tree/main/skills/productivity/grill-me" rel="noopener noreferrer"&gt;mattpocock/skills: grill-me&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Four AI Agent Skills That Make Coding Workflows Sharper</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sat, 08 Aug 2026 23:55:15 +0000</pubDate>
      <link>https://dev.to/shubham399/four-ai-agent-skills-that-make-coding-workflows-sharper-3k5</link>
      <guid>https://dev.to/shubham399/four-ai-agent-skills-that-make-coding-workflows-sharper-3k5</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%2F4159dc16-2571-4bf0-84c9-40044267081e%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%2F4159dc16-2571-4bf0-84c9-40044267081e%2Fpublic" width="1152" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI coding agents are often discussed as though they are a single tool: ask for code, receive code. In practice, useful agent work has stages. You need different behavior when the request is unclear, when a design has to survive scrutiny, when implementation is underway, and when work must move into a new session.&lt;/p&gt;

&lt;p&gt;Trying to solve all four stages with one large prompt usually produces a compromise. The agent may be verbose while you need execution, eager while you need questions, or unable to resume work because the important context is buried in chat history.&lt;/p&gt;

&lt;p&gt;This article covers four skills that address those distinct problems: &lt;strong&gt;Caveman&lt;/strong&gt; for concise execution communication, &lt;strong&gt;Superpowers&lt;/strong&gt; for structured development, &lt;strong&gt;grill-me&lt;/strong&gt; for pressure-testing a proposal, and &lt;strong&gt;handoff&lt;/strong&gt; for transferring the live thread to a fresh agent or session.&lt;/p&gt;

&lt;p&gt;They are complementary. The goal is not to add more ceremony to every edit. It is to apply the smallest useful constraint at the moment it prevents the most waste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four failure modes of AI-assisted development
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The agent starts coding before the work is understood
&lt;/h3&gt;

&lt;p&gt;A request such as “add organization roles” hides decisions about membership, permission scope, migrations, audit trails, errors, and rollout. An agent can produce a plausible patch before any of those choices are explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The agent agrees instead of challenging
&lt;/h3&gt;

&lt;p&gt;Helpful assistants tend to accept a framing. That is dangerous when the framing is a proposal rather than a settled requirement. You need an interview that exposes dependencies and asks what could fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The agent talks too much during routine work
&lt;/h3&gt;

&lt;p&gt;Once a direction is approved, long explanations can become friction. During debugging, review follow-ups, and small implementation loops, the useful output is usually a finding, a change, validation, and a risk note.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Context is lost at a session boundary
&lt;/h3&gt;

&lt;p&gt;A new agent with no context repeats discovery. A new agent with a full transcript has to find the current state among outdated ideas and tool logs. Neither is a reliable way to continue a task.&lt;/p&gt;

&lt;p&gt;The four skills map directly to those problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  grill-me: challenge the proposal before code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-grill-me" rel="noopener noreferrer"&gt;grill-me&lt;/a&gt; is a manually invoked skill that interviews you about a plan or design one question at a time. It provides a recommended answer and waits for feedback. It should inspect the codebase when a fact is available there instead of asking the user to reconstruct it.&lt;/p&gt;

&lt;p&gt;Use it when an idea is plausible but not settled. Good examples include a new API contract, a permission model, a caching strategy, a workflow redesign, or a schema change. The point is not to ask endless hypothetical questions. The point is to resolve the decisions that implementation would otherwise make accidentally.&lt;/p&gt;

&lt;p&gt;Start with a concrete proposal. State the goal, constraints, existing artifacts, and desired output. Ask the agent to inspect relevant files first, then grill the plan one decision at a time. Keep a record of the resulting decisions, non-goals, open risks, and the next artifact to create.&lt;/p&gt;

&lt;p&gt;AI Hero notes that Matt Pocock now generally recommends a broader &lt;strong&gt;domain-model&lt;/strong&gt; workflow when coding plans need to be aligned with a project’s vocabulary, context files, and architecture records. grill-me remains valuable as a focused pressure-test when you want sharp questions rather than a complete modeling process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Superpowers: turn decisions into a controlled delivery process
&lt;/h2&gt;

&lt;p&gt;Once the direction is clear, &lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt; supplies the broader development workflow. Its repository describes a process where an agent clarifies the actual outcome, develops a specification and design, gets approval, creates an implementation plan, emphasizes true red/green TDD, and proceeds through implementation and review. It also supports subagent-driven development for work that has real task boundaries.&lt;/p&gt;

&lt;p&gt;Use Superpowers for work where the cost of an incorrect assumption is meaningful: new features, public API changes, complex bugs, behavioral refactors, migrations, or security-sensitive changes. It is deliberately heavier than the process you need for a one-line typo.&lt;/p&gt;

&lt;p&gt;The key benefit is the presence of gates. Clarification catches the wrong outcome. Design approval catches a poor shape. A plan makes sequencing visible. Red/green tests make selected behavior executable. Review compares the delivered diff with the approved intent rather than merely checking whether the code looks reasonable.&lt;/p&gt;

&lt;p&gt;For a team-invitation feature, the workflow could establish expiry and revocation rules, define authorization boundaries, choose the token lifecycle, plan storage and migration, write failure-case tests, implement in small slices, and review the result against the original product behavior. Each step reduces the chance that a polished patch is solving an unowned problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveman: keep execution communication compact
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/juliusbrussee/caveman" rel="noopener noreferrer"&gt;Caveman&lt;/a&gt; changes an agent’s output style toward concise, direct language. Its README frames the goal as shrinking the agent’s “mouth,” not its “brain,” and says code, commands, and errors remain exact. The project claims 65% fewer output tokens; treat that as a project claim rather than a universal benchmark.&lt;/p&gt;

&lt;p&gt;Caveman belongs after the important decisions have been made. It is excellent for a known-scope bug, a test failure, a terminal-heavy task, or a review follow-up. Instead of several paragraphs of acknowledgement, ask for: root cause, files changed, validation run, result, and remaining risk.&lt;/p&gt;

&lt;p&gt;Concise mode is not the right choice when nuance is itself part of the deliverable. Do not compress discovery, architecture, security analysis, incident communication, or onboarding into slogans. The rule is simple: reduce filler, never evidence.&lt;/p&gt;

&lt;p&gt;A practical operating pattern is to work in normal detail until the design is approved, then switch to concise execution reports. Require the agent to remain explicit about blockers and assumptions. A short warning is useful; an omitted warning is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  handoff: carry the live thread across a boundary
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-handoff" rel="noopener noreferrer"&gt;handoff&lt;/a&gt; creates a compact document that a fresh agent can use to resume work. It captures what is in flight, why it matters, what should happen next, and suggested skills for continuation. Crucially, it references existing specifications, plans, ADRs, issues, commits, and diffs rather than copying them. It is saved to the operating system’s temporary directory, not the workspace, and is intended to redact secrets and personally identifiable information.&lt;/p&gt;

&lt;p&gt;Use it before ending work, near a context limit, when moving a task between agents, or when deliberately resetting a conversation. Pass a note about the next session’s purpose so the handoff is tailored to the next action.&lt;/p&gt;

&lt;p&gt;A good handoff names the objective, completed work, settled decisions, blockers, validation, canonical references, and one precise next action. “Continue tomorrow” is not enough. “Service-layer authorization is complete; inspect the notification adapter before implementing email delivery; the approved spec is at this path; targeted tests pass” is actionable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The integrated workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frame the request.&lt;/strong&gt; State desired outcome, constraints, and non-goals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use grill-me.&lt;/strong&gt; Pressure-test linked decisions and inspect repository evidence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Superpowers.&lt;/strong&gt; Convert settled decisions into an approved design, plan, tests, implementation, and review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Caveman during execution.&lt;/strong&gt; Keep updates compact while preserving findings, commands, test results, and risks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use handoff at transitions.&lt;/strong&gt; Preserve only the resumable thread and point to durable artifacts.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not every task needs every stage. A trivial local change might use none of them. A medium feature may need Superpowers and Caveman. A risky design change may begin with grill-me and end with handoff. The value is in choosing the appropriate control, not in mechanically invoking every tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to adopt the stack
&lt;/h2&gt;

&lt;p&gt;Start small. Pick one feature with meaningful ambiguity. Ask the agent to inspect the relevant code and grill the proposal. Require a reviewable design and test plan. Once approved, switch to short execution reports. At the first planned context switch, create a handoff and ask a fresh agent to continue from it.&lt;/p&gt;

&lt;p&gt;Measure outcomes that matter: how many assumptions were caught before code, whether reviewers can explain why a diff exists, how often agents repeat discovery after a switch, and whether concise reporting shortens the human review loop. Do not judge success only by token count or by how impressive a generated plan looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and guardrails
&lt;/h2&gt;

&lt;p&gt;These skills do not replace accountable engineering judgment. They cannot decide product strategy, guarantee secure systems, or prove that acceptance criteria reflect real user needs. They help make decisions, evidence, and transitions visible.&lt;/p&gt;

&lt;p&gt;Use source control, reviews, tests, observability, and normal release controls. Keep secrets out of conversations and handoffs. Treat agent recommendations as inputs to a decision, not the decision itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Reliable AI-assisted development is not about making an agent behave the same way all the time. Ask it to question when the plan is uncertain, follow a disciplined workflow when the change is consequential, communicate briefly when execution is clear, and hand off cleanly when context changes. Caveman, Superpowers, grill-me, and handoff each make one part of that system sharper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/juliusbrussee/caveman" rel="noopener noreferrer"&gt;Caveman repository&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers repository&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-grill-me" rel="noopener noreferrer"&gt;AI Hero: grill-me&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.aihero.dev/skills-handoff" rel="noopener noreferrer"&gt;AI Hero: handoff&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>The Golden Rule of Payout Systems: Why "Pending" is Never a Failure</title>
      <dc:creator>Shubham</dc:creator>
      <pubDate>Sun, 02 Aug 2026 01:06:48 +0000</pubDate>
      <link>https://dev.to/shubham399/the-golden-rule-of-payout-systems-why-pending-is-never-a-failure-3n6</link>
      <guid>https://dev.to/shubham399/the-golden-rule-of-payout-systems-why-pending-is-never-a-failure-3n6</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%2Fda90968b-1e44-4ff3-a8cb-8ef877fa4bc8.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%2Fda90968b-1e44-4ff3-a8cb-8ef877fa4bc8.png%2Fpublic"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A timeout means your application did not receive a final answer. It does not prove that a bank, payment gateway, or payment partner did not receive or process the payout request.&lt;/p&gt;

&lt;p&gt;That distinction is the foundation of safe payout design. When a payout outcome is uncertain, the system should preserve it as &lt;code&gt;pending&lt;/code&gt; until it receives reliable evidence that the payout completed or failed. Treating uncertainty as failure can make funds available for a second transfer while the first transfer is still moving through the payment rail.&lt;/p&gt;

&lt;p&gt;The result is one of the most expensive errors in payments: a duplicate payout.&lt;/p&gt;

&lt;h2&gt;
  
  
  A payout is not a synchronous request
&lt;/h2&gt;

&lt;p&gt;A simple architecture diagram suggests a clean sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A user requests a payout.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your application calls a payment API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The partner returns success or failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your database records the result.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That model is useful for a happy-path demo. It is incomplete for production money movement.&lt;/p&gt;

&lt;p&gt;A payout can cross several systems before the recipient receives funds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
  |
  v
Your application
  |
  v
Payment partner
  |
  v
Banking or payment rail
  |
  v
Recipient bank
  |
  v
Recipient account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each boundary introduces failure modes that your application cannot observe directly. A network connection may close after the payment partner has accepted the request. A gateway may return an error while its internal worker continues processing. A payment rail may accept an instruction but delay its final result.&lt;/p&gt;

&lt;p&gt;From your application’s perspective, several different events can look exactly the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request sent
  |
  +--&amp;gt; Partner never received it
  |
  +--&amp;gt; Partner received it but did not process it
  |
  +--&amp;gt; Partner accepted it and is still processing it
  |
  +--&amp;gt; Payout completed, but the response never reached you
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout tells you only one thing: the caller has no final response. It says nothing conclusive about whether money movement started.&lt;/p&gt;

&lt;p&gt;That is why a payment API response is not always the final source of truth. The payment flow may continue after the HTTP request has ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state that systems often miss
&lt;/h2&gt;

&lt;p&gt;Many applications model a transaction with two terminal states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;success
failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Payout systems need a third state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In implementation, that state is often named &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;submitted&lt;/code&gt;, or &lt;code&gt;status_unknown&lt;/code&gt;. The label is less important than its behavior.&lt;/p&gt;

&lt;p&gt;An uncertain payout must not behave like a failed payout.&lt;/p&gt;

&lt;p&gt;It should not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Restore funds for a new payout automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trigger a new payout attempt with a new external reference.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Be removed from operational records.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Be treated as resolved because an API call failed locally.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should remain visible and traceable until the system can establish an outcome.&lt;/p&gt;

&lt;p&gt;Consider a timeout after a payout submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payout initiated
  |
  v
Request sent to partner
  |
  v
Network timeout
  |
  v
Outcome unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unsafe implementation turns that timeout into a failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timeout
  |
  v
Mark payout failed
  |
  v
Restore user balance
  |
  v
Allow a new payout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the first request was accepted before the timeout, the user can now create a second payout using money that is already committed to the first one.&lt;/p&gt;

&lt;p&gt;The application has created a duplicate-payment risk because it replaced uncertainty with an unsupported conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a false failure costs more than a delay
&lt;/h2&gt;

&lt;p&gt;Every payout platform balances two risks.&lt;/p&gt;

&lt;p&gt;The first risk is a delayed payout. The user sees a pending state for longer than expected. Support may need to answer questions. The payout may require reconciliation before it reaches a terminal state.&lt;/p&gt;

&lt;p&gt;The second risk is a false failure. The original payout succeeds, but the platform decides it failed and makes the funds available again.&lt;/p&gt;

&lt;p&gt;These risks are not equivalent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delayed resolution
&lt;/h3&gt;

&lt;p&gt;When a payout remains pending:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The user may have to wait.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The funds may remain unavailable temporarily.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Support volume may increase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Operations may need to review exceptions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those costs matter. They are usually contained and reversible.&lt;/p&gt;

&lt;h3&gt;
  
  
  False failure
&lt;/h3&gt;

&lt;p&gt;When a completed payout is marked failed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The recipient may receive money twice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The ledger can show an incorrect available balance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A refund or retry may create another transfer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finance teams may need manual investigation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recovering funds may depend on recipient cooperation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Audit and reporting records become harder to explain.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pending payout creates an operational problem. A false failure can create a financial loss.&lt;/p&gt;

&lt;p&gt;This is why payout systems should prefer delayed certainty over an incorrect terminal state. The default should be conservative:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A payout is not failed until an authoritative source confirms final failure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Build a state machine that preserves uncertainty
&lt;/h2&gt;

&lt;p&gt;The rule must exist in code, not only in documentation. A clear state machine prevents workers, webhooks, API handlers, and support tools from applying conflicting decisions.&lt;/p&gt;

&lt;p&gt;A basic model can look 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;created
  |
  v
submitting
  |
  +--&amp;gt; pending
  |      |
  |      +--&amp;gt; completed
  |      |
  |      +--&amp;gt; failed
  |
  +--&amp;gt; failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important rule is that &lt;code&gt;failed&lt;/code&gt; requires evidence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
  -&amp;gt; submitting
  -&amp;gt; pending
  -&amp;gt; completed

submitting + documented final rejection
  -&amp;gt; failed

submitting + timeout or connection reset
  -&amp;gt; pending

pending + confirmed completion
  -&amp;gt; completed

pending + confirmed final rejection
  -&amp;gt; failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout is not a final rejection. A connection reset is not a final rejection. An unrecognized response is not a final rejection.&lt;/p&gt;

&lt;p&gt;Even an HTTP 5xx response should not automatically mean the payout failed unless the payment partner explicitly documents that response as proof the request was not accepted or processed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate transport status from payout status
&lt;/h3&gt;

&lt;p&gt;Transport signals describe communication between systems. Payment states describe the outcome of money movement. They should not be treated as the same thing.&lt;/p&gt;

&lt;p&gt;Signal&lt;/p&gt;

&lt;p&gt;What it proves&lt;/p&gt;

&lt;p&gt;Safe payout state&lt;/p&gt;

&lt;p&gt;Request accepted by partner&lt;/p&gt;

&lt;p&gt;The partner accepted the submission&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pending&lt;/code&gt; or &lt;code&gt;processing&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Documented terminal success&lt;/p&gt;

&lt;p&gt;The partner reports a completed outcome&lt;/p&gt;

&lt;p&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Documented terminal rejection&lt;/p&gt;

&lt;p&gt;The partner reports the payout cannot proceed&lt;/p&gt;

&lt;p&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Timeout or connection reset&lt;/p&gt;

&lt;p&gt;Your application lacks a response&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pending&lt;/code&gt; or &lt;code&gt;status_unknown&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Invalid or incomplete response&lt;/p&gt;

&lt;p&gt;The response cannot establish outcome&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pending&lt;/code&gt; or &lt;code&gt;status_unknown&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Persist the original payout intent before sending the external request. Keep the fields needed to resolve it later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Internal payout ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Idempotency key&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Attempt ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;External reference, when available&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amount and currency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recipient identifier or beneficiary reference&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Submission time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Latest known external status&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Raw partner response or error details, where appropriate&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not overwrite earlier evidence when a later status check changes the payout state. An audit trail is essential when support or finance teams need to reconstruct what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries need idempotency
&lt;/h2&gt;

&lt;p&gt;Retries are necessary in distributed systems. They are also one of the main ways duplicate payouts occur.&lt;/p&gt;

&lt;p&gt;The core rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Retry an uncertain submission only when the receiving system can identify it as the same payout attempt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That usually means sending a stable idempotency key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payout intent: payout_123
Attempt: attempt_001
Idempotency key: idem_abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request times out, a transport retry should use the same idempotency key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retry submission
  |
  v
Idempotency key: idem_abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receiving system can then return the existing result or continue the original request instead of creating a second payout.&lt;/p&gt;

&lt;p&gt;A new idempotency key changes the meaning of the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retry submission
  |
  v
Idempotency key: idem_xyz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the partner’s behavior, that may be interpreted as a brand-new payout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transport retries and business retries are different
&lt;/h3&gt;

&lt;p&gt;A transport retry attempts to deliver the same request again. It uses the same idempotency key and should refer to the same payout attempt.&lt;/p&gt;

&lt;p&gt;A business retry happens after a confirmed terminal failure. It creates a new payout attempt because the old one is known not to have completed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payout intent: payout_123

Attempt 1
  idempotency key: idem_abc
  final status: failed

Attempt 2
  idempotency key: idem_def
  final status: pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payout intent connects both attempts. The attempt IDs and idempotency keys keep their external submissions distinct.&lt;/p&gt;

&lt;p&gt;Before creating a new business retry, check whether the earlier attempt has a confirmed terminal status. If the old attempt is still unknown, resolve it through a status query or reconciliation process first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;finalFailureConfirmed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="nf"&gt;createNewAttempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payoutIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;outcomeUnknown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="nf"&gt;queryPartnerStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;scheduleReconciliation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nf"&gt;continueNormalProcessing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a provider does not support idempotency, automatic retries after an unknown outcome are unsafe. Queue the payout for review or status resolution instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation resolves what APIs cannot
&lt;/h2&gt;

&lt;p&gt;A synchronous API response captures one point in time. Reconciliation compares your internal records with external payment records after the fact.&lt;/p&gt;

&lt;p&gt;It closes the gap between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What your application thinks happened.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What the payment partner reports.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What the banking or payment rail processed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For payouts that remain pending, reconciliation should compare the details that identify a transaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Internal and external references&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amount and currency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Beneficiary details or recipient reference&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Submission date and time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Latest known partner status&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Settlement or completion records, where the rail provides them&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cadence should match the reporting and settlement behavior of the rail you operate. Some partners provide near-real-time status APIs. Others provide files or reports later. Your system should account for that delay rather than treating it as a failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reconciliation is also an incident control
&lt;/h3&gt;

&lt;p&gt;Reconciliation can reveal problems that request-response processing cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A payout completed after a client timeout.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A payout marked completed without a matching external record.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple external submissions for the same payout intent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A pending payout that has exceeded its expected resolution time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A mismatch between ledger entries and payment records.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every mismatch should enter an exception queue with an owner, an audit note, and a next action. A queue without ownership is only a list of unresolved risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational rules and common mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rules that keep uncertainty safe
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep funds unavailable for a second payout while the first outcome is unknown.&lt;/strong&gt; The exact accounting treatment depends on the product, but the same balance must not support two active payout attempts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make state transitions idempotent.&lt;/strong&gt; A repeated webhook, reconciliation record, or worker retry must not apply ledger changes twice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use webhooks as signals, not the only record.&lt;/strong&gt; Webhooks can arrive late, repeat, or fail to arrive. Combine them with status queries and reconciliation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Record every payout attempt.&lt;/strong&gt; Do not delete ambiguous records. They hold the identifiers required to determine the outcome.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Alert on stale pending payouts.&lt;/strong&gt; Define expected resolution windows by rail and route overdue cases to operations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Give support useful context.&lt;/strong&gt; Show the payout reference, current state, latest update, last status check, and planned next step.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Refunding immediately after a timeout
&lt;/h4&gt;

&lt;p&gt;A timeout can occur after the payment partner accepted the request. Restoring the balance immediately can make the funds available for another payout.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;pending&lt;/code&gt; until a final result is confirmed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Retrying with a new idempotency key
&lt;/h4&gt;

&lt;p&gt;A new key may tell the partner to create another payout rather than resume or return the existing one.&lt;/p&gt;

&lt;p&gt;Reuse the original key for a transport retry. Create a new key only for a confirmed new attempt.&lt;/p&gt;

&lt;h4&gt;
  
  
  Treating HTTP success as settlement
&lt;/h4&gt;

&lt;p&gt;An accepted request is not always a completed payout. It may still be queued, screened, rejected later, or waiting on another system.&lt;/p&gt;

&lt;p&gt;Model acceptance and completion as separate states when the payment rail requires it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Treating a webhook as final proof without validation
&lt;/h4&gt;

&lt;p&gt;A webhook should be authenticated, deduplicated, and checked against the payment partner’s documented status model. It may be delayed or repeated.&lt;/p&gt;

&lt;h4&gt;
  
  
  Removing ambiguous records to keep dashboards clean
&lt;/h4&gt;

&lt;p&gt;An unresolved payout is not noise. It is a financial record that requires a final outcome. Keep it visible until reconciliation resolves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A timeout means the caller lacks an answer. It does not prove a payout failed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Model uncertainty as a durable &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, or &lt;code&gt;status_unknown&lt;/code&gt; state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move a payout to &lt;code&gt;failed&lt;/code&gt; only with reliable evidence of a terminal rejection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use stable idempotency keys for transport retries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Treat confirmed failures as new business attempts, with new attempt records.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reconcile non-final payouts against the authoritative records available for the payment rail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prefer a temporary pending state over a duplicate transfer.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Payout systems must be designed for incomplete information. Networks fail, responses disappear, and external systems may continue processing after your request has timed out.&lt;/p&gt;

&lt;p&gt;The safe response is not to guess.&lt;/p&gt;

&lt;p&gt;When the payout outcome cannot be proven, preserve the state as pending, retain the identifiers needed to investigate it, and reconcile it until a final result is available. That choice may delay resolution, but it prevents the system from turning uncertainty into a duplicate payment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coming Next Week
&lt;/h3&gt;

&lt;p&gt;Next week, I’ll share four &lt;strong&gt;AI agent skills I use to make my coding workflow faster, clearer, and easier to manage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A practical look at the small systems behind better AI-assisted engineering.&lt;/p&gt;

</description>
      <category>payments</category>
      <category>fintech</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <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;TypeScript cannot sanitize HTML, prevent SQL injection, or authorize requests. Those controls must run at runtime.&lt;/p&gt;

&lt;p&gt;It can reduce mistakes inside an application: returning database rows from an API, mixing tenant and user IDs, treating unvalidated JSON as trusted input, or passing vague permission booleans through several layers. The useful pattern is to model trust boundaries explicitly, then keep runtime checks close to the conversion point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TypeScript can and cannot do for security
&lt;/h2&gt;

&lt;p&gt;A type system helps when a security property concerns the shape, origin, or permitted flow of values inside an application. It can distinguish a public API response from a database row. It can reject a &lt;code&gt;ProjectId&lt;/code&gt; where a &lt;code&gt;UserId&lt;/code&gt; is required. It can require a caller to provide a verified access capability before invoking a sensitive operation.&lt;/p&gt;

&lt;p&gt;This moves some failures from production behavior to compiler feedback. A refactor that returns &lt;code&gt;passwordHash&lt;/code&gt; can fail review because the endpoint must return a public DTO. A tenant-scoped repository method can reject arguments from the wrong domain before tests run.&lt;/p&gt;

&lt;p&gt;TypeScript is erased at runtime. An attacker does not send a server a &lt;code&gt;TenantId&lt;/code&gt;; they send JSON, headers, cookies, URLs, and request bodies. A cast such as &lt;code&gt;value as TenantId&lt;/code&gt; is a developer assertion, not proof that the value is valid.&lt;/p&gt;

&lt;p&gt;Keep the responsibility of each control clear:&lt;/p&gt;

&lt;p&gt;Control&lt;/p&gt;

&lt;p&gt;Problem it addresses&lt;/p&gt;

&lt;p&gt;Static types&lt;/p&gt;

&lt;p&gt;Accidental misuse of values in application code&lt;/p&gt;

&lt;p&gt;Runtime schemas&lt;/p&gt;

&lt;p&gt;Malformed or untrusted external data&lt;/p&gt;

&lt;p&gt;Authentication and authorization&lt;/p&gt;

&lt;p&gt;Whether a caller may perform an action&lt;/p&gt;

&lt;p&gt;Parameterized SQL&lt;/p&gt;

&lt;p&gt;SQL injection through query data&lt;/p&gt;

&lt;p&gt;Output encoding and sanitization&lt;/p&gt;

&lt;p&gt;Browser injection risks such as XSS&lt;/p&gt;

&lt;p&gt;Tests, monitoring, and review&lt;/p&gt;

&lt;p&gt;Regressions outside compiler coverage&lt;/p&gt;

&lt;p&gt;Types are one layer. They work when they describe boundaries enforced elsewhere in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model boundaries instead of passing persistence objects around
&lt;/h2&gt;

&lt;p&gt;A common source of accidental exposure is treating an ORM model or database row as the application’s universal user object. A handler loads a user and returns it. Another handler sends the same object to a logger. Later, a migration adds &lt;code&gt;mfaSecret&lt;/code&gt;, &lt;code&gt;passwordHash&lt;/code&gt;, a reset token, support notes, or billing metadata.&lt;/p&gt;

&lt;p&gt;Every serialization site now needs to remember which fields to remove. This deny-list approach fails when the schema changes faster than callers are audited.&lt;/p&gt;

&lt;p&gt;Separate representations based on where data may travel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Persistence model&lt;/strong&gt;: Matches stored data and may include sensitive fields.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Domain model&lt;/strong&gt;: Represents business concepts and operations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Request DTO&lt;/strong&gt;: Represents data accepted by a specific endpoint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Response DTO&lt;/strong&gt;: Represents data intentionally exposed by an endpoint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log event&lt;/strong&gt;: Contains fields approved for observability.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every entity needs each representation. The split has the most value for users, credentials, payments, tenancy, permissions, and objects with internal-only state.&lt;/p&gt;

&lt;p&gt;This database row must never be emitted directly at an HTTP boundary:&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="nl"&gt;passwordResetToken&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A response DTO states what the endpoint may reveal:&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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important decision is the allow-list. Adding a column to &lt;code&gt;UserRow&lt;/code&gt; does not add it to &lt;code&gt;PublicUserDto&lt;/code&gt;. A new sensitive field remains private until someone intentionally exposes it.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCurrentUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&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;user&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;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&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="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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mapper is a review point. Requirements to add &lt;code&gt;displayName&lt;/code&gt; or an avatar URL appear as a small, visible DTO change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply the same rule to logs
&lt;/h3&gt;

&lt;p&gt;Logging arbitrary objects is another data-exposure boundary. This code is easy to add while debugging and hard to audit later:&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;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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login attempt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It may record credentials, tokens, personal data, or columns added by a later migration. Prefer event-shaped logs:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&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="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="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;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="nx"&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;login attempt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For failures, log a stable error code instead of the raw request body. If support workflows need selected input, create a redaction function and test that sensitive fields never appear.&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;LoginFailureAudit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;emailDomain&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_credentials&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;account_locked&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;function&lt;/span&gt; &lt;span class="nf"&gt;toLoginFailureAudit&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginFailureAudit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reason&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;LoginFailureAudit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;emailDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domain&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="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logger redaction settings and ORM serialization hooks are useful backup controls. The log call site should still show the intended data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use DTOs and allow-list serialization
&lt;/h2&gt;

&lt;p&gt;Explicit DTOs prevent another problem: endpoint contracts changing because internal models change. ORM relations, joins, virtual fields, and serializer defaults can expose fields the endpoint author did not intend to publish.&lt;/p&gt;

&lt;p&gt;Keep response mapping near the API layer. Repository methods should return information needed by the domain layer. Controllers or presentation modules should decide the public contract.&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;ProjectRow&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;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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;visibility&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="o"&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="nl"&gt;archivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&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;billingAccountId&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;type&lt;/span&gt; &lt;span class="nx"&gt;ProjectDto&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;visibility&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="o"&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;function&lt;/span&gt; &lt;span class="nf"&gt;toProjectDto&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;ProjectRow&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ProjectDto&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;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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;span class="na"&gt;visibility&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;visibility&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This repetition records a policy decision. &lt;code&gt;tenantId&lt;/code&gt;, &lt;code&gt;archivedAt&lt;/code&gt;, and &lt;code&gt;billingAccountId&lt;/code&gt; are not part of this API contract.&lt;/p&gt;

&lt;p&gt;For larger APIs, use a presentation layer or serializer module. Avoid generic &lt;code&gt;pick()&lt;/code&gt; helpers that scatter unreviewed field lists across the codebase. Named mappers are easier to search, test, and review.&lt;/p&gt;

&lt;p&gt;Integration tests should verify real JSON responses, not only TypeScript assignments. Types cannot protect an endpoint that bypasses the mapper or an ORM plugin that serializes a model directly.&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;does not expose credential fields&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v1/me&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&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;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;person@example.com&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passwordHash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mfaSecret&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make identifiers and authority explicit
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;string&lt;/code&gt; does not state whether it identifies a tenant, user, project, invoice, or external provider account. Plain strings are easy to mix, especially in multi-tenant services where a missing tenant constraint can expose another customer’s data.&lt;/p&gt;

&lt;p&gt;Branded types add nominal meaning to TypeScript’s structural type system:&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="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectBrand&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;type&lt;/span&gt; &lt;span class="nx"&gt;ProjectId&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;projectBrand&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A repository signature can then state its scope:&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;async&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="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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;loadUser(tenantId, projectId)&lt;/code&gt; is a type error. This does not prove the query is correct, but it prevents swapped-argument bugs that can look valid in review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Construct brands only after validation
&lt;/h3&gt;

&lt;p&gt;Do not export a generic conversion helper that converts any string into any brand. It defeats the purpose. Create domain values after validating data at a boundary.&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;function&lt;/span&gt; &lt;span class="nf"&gt;asTenantId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TenantId&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="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f-&lt;/span&gt;&lt;span class="se"&gt;]{36}&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid tenant ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, prefer a UUID parser or the identifier format used by the system instead of maintaining a broad regular expression. One small module should own the cast and validate before casting.&lt;/p&gt;

&lt;p&gt;Brands provide no runtime protection. Database constraints, query predicates, row-level security where appropriate, and authorization checks remain necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model verified authority as a capability
&lt;/h3&gt;

&lt;p&gt;A boolean such as &lt;code&gt;canEdit&lt;/code&gt; is easy to pass through several layers, disconnect from its resource, or accidentally derive from untrusted input. A capability type keeps the checked user and resource together.&lt;/p&gt;

&lt;p&gt;Use a private brand so modules importing &lt;code&gt;ProjectEditor&lt;/code&gt; cannot construct it with a normal object literal.&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;// project-access.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectEditorBrand&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="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;projectEditor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;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="k"&gt;readonly&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;UserId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;projectId&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;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;projectEditorBrand&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&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;requireProjectEditor&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="nx"&gt;projectId&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProjectEditor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;membership&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;memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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;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;membership&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;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="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="nx"&gt;userId&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="nx"&gt;projectEditorBrand&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&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;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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&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;projectId&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request handler first authenticates the caller, then loads trusted membership data, then obtains the capability:&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;const&lt;/span&gt; &lt;span class="nx"&gt;access&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;requireProjectEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&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;projectId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&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;input&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;Keeping the brand private prevents importing modules from constructing this value with an object literal. It does not prevent unsafe casts. Authorization still depends on trusted runtime checks.&lt;/p&gt;

&lt;p&gt;Every relevant request must verify identity, tenant membership, ownership, and current policy. A stale role cache, an incorrectly scoped lookup, or a cast can still create an authorization flaw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate data before creating domain values
&lt;/h2&gt;

&lt;p&gt;External values are &lt;code&gt;unknown&lt;/code&gt; until checked. This includes more than HTTP request bodies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;URL parameters and query strings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Headers, cookies, and bearer-token claims&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment variables&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Webhook payloads&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Queue messages and event streams&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Imported CSV files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third-party API responses&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database data when schemas or migrations are not fully trusted&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TypeScript interfaces do not validate any of these values. A request annotated as &lt;code&gt;CreateProjectInput&lt;/code&gt; still arrives as bytes over the network.&lt;/p&gt;

&lt;p&gt;Use a runtime schema at the boundary, then infer the TypeScript type from that schema. This avoids maintaining two sources of truth.&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;CreateProjectSchema&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="nf"&gt;strict&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;CreateProjectSchema&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;CreateProjectSchema&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;&lt;code&gt;.strict()&lt;/code&gt; is a policy choice. Rejecting unknown keys can catch client bugs and prevent accidental acceptance of fields such as &lt;code&gt;ownerId&lt;/code&gt; or &lt;code&gt;role&lt;/code&gt;. Some public APIs intentionally ignore unknown fields for forward compatibility. Choose and document the behavior.&lt;/p&gt;

&lt;p&gt;Schema validation checks shape and local constraints. It does not enforce business rules that depend on current state. A valid project name may already be in use. A valid &lt;code&gt;projectId&lt;/code&gt; may belong to another tenant. A valid visibility value may be unavailable to the caller’s plan or role.&lt;/p&gt;

&lt;p&gt;A safe request flow is usually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Authenticate the request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Parse and validate untrusted syntax and shape.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Convert validated primitives into domain values where needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Load current resource and membership state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authorize the requested operation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apply business rules and write through scoped data access.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Set resource limits separately. A schema that allows a 120-character string does not limit the HTTP request body. Configure body-size limits, upload limits, timeouts, queue payload limits, and rate limits at the relevant layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep SQL parameterized
&lt;/h2&gt;

&lt;p&gt;Types can clarify repository inputs. They do not make string interpolation safe. SQL injection prevention depends on a database driver or query builder that sends data separately from SQL syntax.&lt;/p&gt;

&lt;p&gt;Use parameter placeholders for values:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not interpolate values into query text, even if the values have branded types or passed a schema:&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;// Unsafe. The value becomes SQL source text.&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="s2"&gt;`SELECT * FROM users WHERE id = '&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="s2"&gt;'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A UUID validation rule may reduce risk in one path. It does not make interpolation a safe convention. Validation rules change, other inputs may have weaker constraints, and escaping varies by database and driver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic identifiers need an allow-list
&lt;/h3&gt;

&lt;p&gt;Placeholders generally cannot represent SQL identifiers or keywords. If an endpoint supports sorting by selected columns, construct that fragment only from source-owned literals.&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;const&lt;/span&gt; &lt;span class="nx"&gt;sortColumns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;SortKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;sortColumns&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;parseSortKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SortKey&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;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sortKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseSortKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&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;sortColumn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sortColumns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sortKey&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;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="s2"&gt;`SELECT id, name FROM projects WHERE tenant_id = $1 ORDER BY &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sortColumn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ASC`&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request controls a known key, not arbitrary SQL. Apply the same approach to sort direction, table selection, and other dynamic SQL fragments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat HTML as a runtime trust boundary
&lt;/h2&gt;

&lt;p&gt;Most rendering should use framework escaping. React escapes strings rendered in JSX by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The risk appears when an application intentionally renders HTML, such as rich-text content or imported documentation. Raw user HTML must not reach &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; without a reviewed sanitization step.&lt;/p&gt;

&lt;p&gt;A brand makes the distinction visible:&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The brand alone has no security value:&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;const&lt;/span&gt; &lt;span class="nx"&gt;unsafe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That cast only changes the compile-time label. The constructor must run a maintained sanitizer with an application-specific policy.&lt;/p&gt;

&lt;p&gt;For Node.js or server-side rendering, DOMPurify needs a DOM implementation. The following setup uses JSDOM:&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="nx"&gt;createDOMPurify&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;dompurify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JSDOM&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;jsdom&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="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JSDOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;window&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;DOMPurify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDOMPurify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ALLOWED_TAGS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p&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;a&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;strong&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;em&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;ul&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;ol&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;li&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;code&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;pre&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;ALLOWED_ATTR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clean&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rendering component can require sanitized content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RichText&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;span class="nl"&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the sanitizer factory server-only when using SSR. Browser bundles can initialize DOMPurify with the browser &lt;code&gt;window&lt;/code&gt; object instead.&lt;/p&gt;

&lt;p&gt;Sanitizer settings are security-sensitive code. Review allowed tags, attributes, URL protocols, CSS handling, embedded content, and server-rendering behavior. Keep DOMPurify and JSDOM updated. If the product does not need arbitrary HTML, use Markdown with a restricted renderer or a structured rich-text format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adopt the pattern without type theater
&lt;/h2&gt;

&lt;p&gt;Security-focused types lose value when every escape hatch is open. &lt;code&gt;any&lt;/code&gt;, broad &lt;code&gt;as&lt;/code&gt; casts, non-null assertions, and generic conversion helpers can erase the protections. Some are necessary at isolated interoperability boundaries. They should not be the usual way to satisfy the compiler.&lt;/p&gt;

&lt;p&gt;Start at boundaries with direct security consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HTTP responses and logs:&lt;/strong&gt; Add response DTOs and named log events for user, payment, admin, and tenancy data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tenant-scoped access:&lt;/strong&gt; Introduce distinct identifiers where swapped values or missing scope could expose another customer’s data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;External inputs:&lt;/strong&gt; Parse request bodies, webhooks, queues, and environment values with runtime schemas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQL access:&lt;/strong&gt; Standardize parameterized database APIs and fixed allow-lists for dynamic identifiers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rich-text rendering:&lt;/strong&gt; Keep raw and sanitized HTML separate, with one narrow sanitization module.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not brand every string. A &lt;code&gt;CurrencyCode&lt;/code&gt;, &lt;code&gt;TenantId&lt;/code&gt;, or &lt;code&gt;SanitizedHtml&lt;/code&gt; type justifies its maintenance cost when mixing it with another value can create a real bug. A type used everywhere but protecting nothing adds friction without improving review quality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce conventions with tooling and tests
&lt;/h3&gt;

&lt;p&gt;Compiler settings help expose unsafe assumptions:&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;"compilerOptions"&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;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedIndexedAccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exactOptionalPropertyTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noImplicitOverride"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;These options are not security controls. They reduce ambiguity around &lt;code&gt;undefined&lt;/code&gt;, optional fields, and inheritance, making boundary code easier to reason about.&lt;/p&gt;

&lt;p&gt;Use linting and review rules for dangerous escape hatches. Examples include requiring justification for &lt;code&gt;as&lt;/code&gt;, prohibiting &lt;code&gt;any&lt;/code&gt; outside approved modules, and flagging direct model serialization in controllers. Keep exceptions narrow and documented.&lt;/p&gt;

&lt;p&gt;Test what types cannot prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An authenticated user cannot read or modify another tenant’s resource.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive columns do not appear in real HTTP responses or logs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Webhook verification occurs before payload use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL repository methods retain tenant predicates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sanitized rich text rejects unsafe URLs and event handlers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role changes and revoked memberships take effect as expected.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration tests should exercise real middleware, serializers, and database paths. Mapper unit tests help, but integration tests catch endpoints that bypass the mapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;TypeScript improves application security when it represents meaningful boundaries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Return explicit response DTOs rather than persistence objects. Allow-list fields that may leave the service.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create dedicated log events and redaction functions. Do not log arbitrary models or request bodies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use branded identifiers for values that must not be mixed, especially in tenant-scoped code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Model verified authority with capability types, but check authorization from trusted state for every relevant request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Parse external values at runtime before treating them as domain input.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep SQL parameterized. Use fixed source-owned allow-lists for dynamic identifiers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Escape HTML by default. Sanitize only when rendering HTML is required.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Back type design with integration tests, database constraints, dependency maintenance, monitoring, and security review.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Explicit DTOs, scoped identifiers, narrow authority-bearing APIs, and visible trust conversions make unsafe flows easier to spot during implementation and review. They do not replace runtime defenses. They reduce the chance that ordinary application mistakes bypass those defenses.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.typescriptlang.org/docs/handbook/2/narrowing.html" rel="noopener noreferrer"&gt;TypeScript handbook: narrowing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Injection Prevention Cheat Sheet&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Cross Site Scripting Prevention Cheat Sheet&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Authorization Cheat Sheet&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/cure53/DOMPurify" rel="noopener noreferrer"&gt;DOMPurify documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/jsdom/jsdom" rel="noopener noreferrer"&gt;JSDOM documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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"&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>
  </channel>
</rss>
