Hidden Partitioning: How Iceberg Eliminates Accidental Full Table Scans

Exposed partitioning in Hive versus hidden partitioning in Iceberg showing the same pruning with different user experience

This is Part 5 of a 15-part Apache Iceberg Masterclass. Part 4 covered partition evolution. This article covers hidden partitioning, the feature that ensures users never need to know how their data is physically organized.

The most expensive mistake in data lake querying is the accidental full table scan: a query that reads every file because the user did not correctly reference the partition columns. In Hive, this happens constantly. In Iceberg, it is structurally impossible because users never reference partition columns at all.

Table of Contents

  1. What Are Table Formats and Why Were They Needed?
  2. The Metadata Structure of Current Table Formats
  3. Performance and Apache Iceberg’s Metadata
  4. Technical Deep Dive on Partition Evolution
  5. Technical Deep Dive on Hidden Partitioning
  6. Writing to an Apache Iceberg Table
  7. What Are Lakehouse Catalogs?
  8. Embedded Catalogs: S3 Tables and MinIO AI Stor
  9. How Iceberg Table Storage Degrades Over Time
  10. Maintaining Apache Iceberg Tables
  11. Apache Iceberg Metadata Tables
  12. Using Iceberg with Python and MPP Engines
  13. Streaming Data into Apache Iceberg Tables
  14. Hands-On with Iceberg Using Dremio Cloud
  15. Migrating to Apache Iceberg

The Accidental Full Scan Problem

Exposed partitioning in Hive versus hidden partitioning in Iceberg showing the same pruning with different user experience

In Hive, a table partitioned by year, month, and day requires queries to filter on those exact columns:

-- Hive: This prunes correctly
SELECT * FROM orders WHERE year = 2024 AND month = 3 AND day = 15

-- Hive: This scans EVERYTHING (no pruning)
SELECT * FROM orders WHERE order_date = '2024-03-15'

The second query reads every partition because Hive does not know that order_date maps to the year, month, and day partition columns. There is no error, no warning. The query simply runs 100x slower than it should.

This happens because Hive partitioning is “exposed.” The physical partition columns (year, month, day) are separate from the source column (order_date). Users must understand this mapping and construct their filters accordingly.

How Iceberg Hides Partitioning

Iceberg flips this model. Users filter on the source column (order_date), and the engine automatically maps the filter to the partition values using transform functions.

-- Iceberg: This prunes correctly. Always.
SELECT * FROM orders WHERE order_date = '2024-03-15'

The table’s partition spec declares: PARTITIONED BY (day(order_date)). When the engine processes this query, it:

  1. Reads the partition spec from the table metadata
  2. Applies the day() transform to the filter value: day('2024-03-15') = 2024-03-15
  3. Checks manifest entries for files with matching partition values
  4. Skips every file whose partition value is not 2024-03-15

The user writes natural SQL against the source columns. The engine handles the physical-to-logical mapping. This is why it is called “hidden” partitioning: the partition structure is invisible to the user.

The Six Transform Functions

Iceberg's six partition transform functions showing how each maps source values to partition values

Iceberg defines six partition transforms that map source column values to partition values:

Temporal Transforms

Temporal Transforms

The temporal transforms are hierarchical. If a table is partitioned by day(ts) and a user filters WHERE ts >= '2024-03-01' AND ts < '2024-04-01', the engine recognizes this as a range of days and prunes to only the 31 matching partitions. Engines like Dremio handle this mapping automatically for equality, range, and IN-list predicates.

Value Transforms

Value Transforms

truncate(N, col) takes the first N characters of a string (or truncates a number to a width). This is useful when you want to group data by a string prefix without creating one partition per unique value.

bucket(N, col) applies a hash function and mod N to produce a bucket number from 0 to N-1. This distributes data evenly across a fixed number of buckets, regardless of the column’s value distribution. It is the go-to transform for high-cardinality columns like user_id or order_id where identity partitioning would create millions of tiny partitions.

The Identity Transform

The identity transform (identity(col)) uses the raw column value as the partition value. This is equivalent to Hive-style partitioning, but the column is still “hidden” because the engine handles the mapping. It is useful for low-cardinality columns like region or status where each unique value should be its own partition.

How Pruning Works Under the Hood

Step-by-step flow showing how the engine maps a user query through the partition spec to prune files

The pruning process works in three phases:

Phase 1: Predicate translation. The engine examines each WHERE clause predicate and checks if the filtered column is part of the partition spec. If order_date is the source column for day(order_date), the engine can translate order_date = '2024-03-15' into a partition filter.

Phase 2: Manifest list evaluation. The manifest list stores partition value ranges per manifest. The engine checks if each manifest’s range includes the target partition value. Manifests whose range does not overlap are skipped entirely.

Phase 3: Manifest entry evaluation. For each surviving manifest, the engine checks individual file entries. Only files whose partition value matches 2024-03-15 are included in the scan plan.

This is the same pruning cascade described in Part 3, but now the partition values were derived automatically from the user’s filter on a source column.

Choosing the Right Transform

The choice of partition transform depends on data volume and query patterns:

Choosing the Right Transform

The goal is to create partitions that are large enough to contain optimally-sized files (128-512 MB each) but small enough that partition pruning eliminates most files for typical queries.

Over-partitioning (too many small partitions) creates the small file problem: thousands of tiny files that bloat metadata and slow query planning. Under-partitioning (too few large partitions) reduces pruning effectiveness because each partition contains too much data.

Combining Transforms

Iceberg supports multi-column partition specs:

CREATE TABLE events (
  event_id BIGINT,
  event_time TIMESTAMP,
  user_id BIGINT,
  event_type STRING
) PARTITIONED BY (day(event_time), bucket(32, user_id))

This creates a two-dimensional partition space: each combination of day and user bucket is a separate partition. Queries filtering on event_time get day-level pruning. Queries filtering on user_id get bucket-level pruning. Queries filtering on both get pruning from both dimensions.

Dremio supports all Iceberg transform functions and automatically applies pruning for any combination of partition columns in the query’s WHERE clause.

Why This Matters for Teams

Hidden partitioning changes the operational model for data teams:

Data engineers define the partition strategy once in the table’s partition spec. They can change it later through partition evolution without breaking anything.

Analysts and data scientists write natural SQL against the business columns they understand. They never need to know whether the table is partitioned by day, month, or bucket. Their queries are automatically optimized.

BI tools and dashboards connect to Iceberg tables and issue standard SQL. The tools do not need to understand Iceberg’s partitioning; the engine handles the optimization. This is why hidden partitioning is essential for self-service analytics platforms like Dremio.

The net result: no accidental full table scans, no partition-aware query patterns required from users, and the ability to change the physical layout without impacting any downstream consumer. Part 6 covers what happens when data is written to an Iceberg table.

Books to Go Deeper

Free Resources

Frameworks Are No Longer Being Designed Only for Humans

This is a submission for the Google I/O Writing Challenge

TL;DR
After watching the Google I/O 2026 sessions, I started noticing the same pattern across Flutter, Search, Chrome DevTools, and Google’s agent workflows.

AI is not only becoming part of applications.

Frameworks, tooling, and even the web itself are slowly starting to evolve around AI systems as active builders too.

Estimated read time: ~8 minutes

Table of Contents

  • I thought this year would be about models
  • The Flutter update that changed how I looked at the rest of I/O
  • We used to build interfaces for people
  • Search is starting to behave more like software generation
  • The Human Clipboard Problem
  • Websites are changing too
  • The tension underneath all of this
  • What developers may actually need to adapt to
  • Final thoughts
  • References
  • 🤝 Stay in Touch

I thought this year would be about models

Every Google I/O eventually returns to the same themes.

Better models.

Faster responses.

More capable AI systems.

So when I started watching the Google I/O 2026 sessions, I expected another version of that story.

And there was plenty of it.

But after watching:

another pattern slowly started showing up underneath everything else.

Not through one major announcement.

Not through one headline feature.

It appeared through smaller architectural decisions spread across different sessions.

At first, none of them seemed connected.

Then the Flutter session started talking about separating Material and Cupertino from the core framework.

After that, I started noticing the same idea everywhere else too.

The Flutter update that changed how I looked at the rest of I/O

On the surface, the Flutter announcements sounded like framework maintenance.

Material and Cupertino becoming separate packages.

A more style-neutral Flutter core.

The GenUI SDK.

The A2UI protocol.

But the more I listened, the less it sounded like ordinary framework work.

Because UI frameworks have always been opinionated by design.

That is their job.

They guide developers toward structure and consistency.

For years, frontend development has mostly followed the same model:

developers design interfaces first

users interact with them later

The screens are already decided.

The flows are already decided.

Even dynamic applications are usually built from predefined layouts assembled by humans.

But many of the announcements at Google I/O 2026 seemed to move in another direction.

Interfaces that adapt:

  • to context
  • to conversation
  • to intent
  • to environment
  • to the task happening in that moment

Not fixed screens.

Generated ones.

And that changes something underneath the framework itself.

Because if software is expected to generate interfaces dynamically, frameworks cannot remain tightly structured around predefined human layouts.

They need to become flexible enough for software to assemble software.

That was the moment the rest of I/O started looking different to me.

Most of that realization came from watching What’s New in Flutter.

The session spends a good amount of time talking about GenUI, A2UI, and Flutter becoming more style-neutral instead of tightly coupled to predefined design systems.

The more I watched it, the more it sounded less like a normal framework update and more like Flutter preparing for dynamically generated interfaces.

We used to build interfaces for people

What started becoming more obvious across the sessions was how deeply human-centered most software tooling has always been.

Component systems.

Design systems.

Navigation patterns.

Accessibility flows.

Everything was built around how humans:

  • read
  • navigate
  • interpret
  • scan
  • and move through interfaces visually

But now there is another participant inside the system.

Agents.

And they interact with software differently.

An AI agent does not care whether a button feels visually balanced on a screen.

It cares about:

  • structure
  • interaction points
  • capabilities
  • semantic meaning
  • machine-readable workflows
  • accessible pathways

That changes what frameworks need to optimize for.

Not replacing humans.

But supporting both humans and agents at the same time.

And I think that may be one of the biggest shifts underneath this year’s I/O announcements.

For the first time, frameworks no longer felt entirely human-first.

Google was not only showing how AI could build software.

The more sessions I watched, the more it started feeling like software ecosystems themselves may slowly reorganize as AI systems become active participants inside them.

Search is starting to behave more like software generation

One of the moments I kept returning to from the Google I/O ’26 Keynote was watching Search generate interactive software experiences directly inside the results.

Not just summaries.

Not just responses.

Actual usable interfaces.

A planner.

A simulator.

A generated workflow.

The important part was not the interface itself.

It was what produced it.

Traditionally, developers built applications and search engines helped users discover them.

Now the platform itself is beginning to generate software experiences dynamically for specific situations.

Not permanent applications.

Temporary ones.

Software created for the exact moment it is needed.

The examples shown during the Google I/O ’26 Keynote are probably the clearest way to understand this direction.

Watching Search generate interactive planners and visual simulators in real time felt very different from traditional search demos.

It felt closer to software being assembled during the interaction itself.

And once software starts behaving like this, the frameworks underneath it have to change too.

The Human Clipboard Problem

Another moment I kept thinking about came from Chrome DevTools for Agents.

Mostly because it addressed a workflow almost every developer using AI tools already knows.

You ask AI to generate code.

Something breaks.

You copy the error.

Paste it back into the assistant.

Wait for another response.

Then repeat the cycle again.

The developer becomes the connection between:

  • the runtime
  • the browser
  • the tooling
  • and the AI system

But now agents can:

  • inspect the DOM
  • run Lighthouse audits
  • read error logs
  • attempt fixes
  • validate changes themselves

That changes the role of the developer too.

Because once AI systems can directly interact with tooling, they stop behaving like passive assistants.

They start participating inside the workflow itself.

This part came from the Developer Keynote (Google I/O ’26), especially the Chrome DevTools for Agents demonstrations.

That session introduced one of the most practical shifts from the entire event for me, mostly because it changes how AI interacts with debugging and runtime tooling itself.

Websites are changing too

The same pattern appeared again with WebMCP.

For most of the web’s history, websites were designed almost entirely for human interaction.

Humans:

  • navigate menus
  • press buttons
  • read layouts
  • move through interfaces visually

But WebMCP introduces the idea that websites should expose capabilities directly to agents too.

Not visually.

Structurally.

The Developer Keynote referred to this as improving “Agent Experience.”

And the more I thought about it, the more important that phrase started feeling.

Because once software ecosystems begin optimizing for agents too, the internet itself starts becoming something different.

Not only human-readable.

Machine-readable in a much deeper way.

The conversations around WebMCP and “Agent Experience” also came from the Developer Keynote (Google I/O ’26).

I found myself returning to that part of the session because it changes how the web itself is expected to behave when agents become part of normal software workflows.

The tension underneath all of this

I do not think any of this means:

  • designers disappear
  • frontend developers disappear
  • or interfaces become unpredictable AI-generated chaos

Actually, one of the most interesting parts across these announcements was the tension between:

  • flexibility
  • and consistency

Generated interfaces are powerful because they can adapt dynamically.

But people still rely on:

  • familiarity
  • stable navigation
  • predictable interaction patterns
  • consistent visual systems

That tension matters.

Because completely unrestricted generated interfaces would become exhausting very quickly.

And Google seemed aware of that throughout the sessions.

The direction did not feel like:
“remove structure completely.”

It felt more like:
“allow software to adapt within carefully defined boundaries.”

That is a very different idea.

And probably a much more practical one too.

What developers may actually need to adapt to

I do not think the biggest shift here is that AI will generate more code.

That conversation already exists everywhere.

The more interesting shift is that software ecosystems themselves are beginning to evolve around AI participation.

Frameworks are becoming more flexible.

Developer tools are becoming agent-aware.

Web standards are becoming machine-readable.

Interfaces are becoming dynamic.

And developers may slowly spend less time:

  • manually assembling static screens
  • wiring repetitive workflows
  • acting as the bridge between tools and AI systems

And more time:

  • defining boundaries
  • designing primitives
  • shaping behavior
  • reviewing generated systems
  • guiding adaptive workflows

That feels like a very different model of software development.

Not fully automated.

But definitely different from the one most of us learned.

Because the biggest change may not be AI replacing software development.

It may be software ecosystems slowly reorganizing themselves around AI participation.

Final thoughts

Going into Google I/O 2026, I expected better AI systems.

What I did not expect was how many sessions were pointing toward the same deeper shift underneath them.

Flutter becoming more style-neutral.

Search generating interfaces dynamically.

Web standards exposing capabilities directly to agents.

Developer tooling evolving around autonomous workflows.

Individually, these announcements seemed unrelated.

But together, they started feeling like pieces of the same transition.

For years, frameworks were designed almost entirely around human developers building interfaces for human users.

Now software ecosystems are beginning to evolve around AI systems too.

And I think future developers may look back at this moment less as:
“the year AI became smarter”

and more as:

“the year software ecosystems started reshaping themselves around AI participation.”

References

These sessions helped shape most of the ideas and observations in this article:

I also found these sessions helpful while thinking through the broader direction around AI workflows, tooling, and interface generation:

🤝 Stay in Touch

Place Find me here
GitHub building things → hemapriya-kanagala
LinkedIn resources & updates → hemapriya-kanagala
X random dev thoughts → @KanagalaHema

And seriously, if something here made sense (or didn’t), drop a comment.

Azure DevOps Structure Explained: Organizations, Projects, and Repos Without the Mess

Azure DevOps editorial cover

Azure DevOps editorial cover

A lot of Azure DevOps confusion starts before the first pipeline even runs.

Teams open the portal, create a few repos, maybe a couple of boards, and keep moving. That works for a while. Then a few months later, nobody is fully sure which project owns what, where the shared code should live, or whether a new team should create another project or just add one more repo somewhere random.

That is usually not a tooling problem.

It is a structure problem.

If your Azure DevOps layout is clean, permissions stay easier to manage, pipelines stay easier to reason about, and new developers can understand the system faster.

In this post, I will break down the practical hierarchy in simple words:

  • Organization
  • Project
  • Repository

And more importantly, I will explain how I think about the boundaries between them.

The basic hierarchy

Azure DevOps usually works like this:

  1. Organization is the top-level container
  2. Projects sit inside the organization
  3. Repos sit inside projects
  4. Pipelines, boards, artifacts, and permissions usually attach at the project level and then get refined further

Here is the structure visually:

Azure DevOps organization project repo diagram

What an organization should represent

An Azure DevOps organization is the highest-level workspace.

In most real setups, one organization maps to one company, one startup, one internal platform group, or one major business unit.

Think of it as the outer shell that owns:

  • users
  • billing
  • global access patterns
  • the set of projects your teams work in

A good default is:

  • one company = one organization

You usually do not want a new organization for every product or every team unless there is a strong reason such as:

  • legal separation
  • isolated billing
  • hard security boundaries
  • completely different admin ownership

If you split organizations too early, you create extra admin work and make cross-team visibility worse.

What a project should represent

A project is where most teams should make their main structural decisions.

This is the level where Azure DevOps starts to feel like a working delivery environment instead of just a container.

A project commonly groups together:

  • boards
  • repos
  • pipelines
  • service connections
  • artifacts
  • team-specific permissions
  • delivery workflows

The clean mental model is:

  • one project = one product, platform, or delivery stream

That means a project should represent a chunk of work that has enough internal connection to belong together.

Examples:

  • customer web platform
  • internal data platform
  • mobile app ecosystem
  • infrastructure automation platform

That does not always mean one project per microservice.

If you create too many projects, you make navigation, permissions, reporting, and pipeline management more annoying than they need to be.

What a repo should represent

A repo is where the actual code lives.

Inside a project, you can have one repo or many repos depending on how your system is built.

A repo might represent:

  • one application
  • one backend service
  • one frontend
  • one infrastructure codebase
  • one shared SDK or library

A healthy repo split usually follows ownership and deployment boundaries.

Good repo examples inside a project:

customer-platform/
  frontend-app
  backend-api
  infra-terraform
  shared-components

That split makes sense when those codebases:

  • change at different speeds
  • deploy independently
  • have different owners
  • need different pipeline logic

If everything always ships together and is maintained by the same small team, a monorepo can also be totally fine.

A practical way to decide the boundary

Here is the shortcut I use.

Use a new organization when:

  • admin ownership is fully different
  • billing or compliance must be isolated
  • the systems should not share visibility by default

Use a new project when:

  • a product or platform has its own delivery lifecycle
  • the team wants separate boards, pipelines, and permissions
  • the work is important enough to deserve its own operating space

Use a new repo when:

  • code should version separately
  • deployment should happen separately
  • ownership is different
  • the codebase is becoming noisy or hard to review as one unit

That is the cleanest rule set for most teams.

Example setup that scales well

Say your company has three main delivery areas:

  • customer-facing web product
  • mobile app stack
  • data platform

A clean Azure DevOps layout could look like this:

Organization: nimay-tech

Project: Customer Platform
  Repo: frontend-app
  Repo: backend-api
  Repo: infra-terraform

Project: Mobile Platform
  Repo: mobile-app
  Repo: shared-sdk

Project: Data Platform
  Repo: data-platform

That structure works because each project has a clear mission, and each repo has a clear code boundary.

It is easy to answer:

  • where the mobile shared code lives
  • where the web infra code lives
  • which project owns the related pipelines
  • which team should get access

That clarity saves time every single week.

The mistake people make too early

The most common mistake is either:

  • throwing everything into one giant project forever
  • splitting into too many projects too early

Both create pain.

If everything is in one giant project

You usually get:

  • messy repo lists
  • unclear board ownership
  • too many pipelines in one place
  • broad permissions that are harder to manage
  • shared noise across unrelated teams

If you create too many projects too early

You usually get:

  • duplicated setup work
  • scattered dashboards
  • more service connection management
  • harder cross-team reporting
  • random inconsistency in how teams configure things

The win is not maximum separation.

The win is useful separation.

How pipelines fit into this

Pipelines usually follow the repo and project structure.

A common pattern is:

  • repo-specific build pipeline
  • environment-specific release or deploy stages
  • project-level visibility for delivery flow

For example:

trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

steps:
  - script: npm ci
  - script: npm test
  - script: npm run build

That pipeline belongs naturally with the repo that owns the code. But it still benefits from project-level organization because related teams can find the rest of the delivery assets in one place.

Monorepo vs multi-repo in Azure DevOps

This question comes up a lot.

There is no universal winner.

Monorepo is usually better when:

  • one team owns the whole system
  • components change together often
  • shared tooling matters more than separation
  • the deployment model is still simple

Multi-repo is usually better when:

  • teams own different services
  • services deploy independently
  • access should differ across codebases
  • the platform is growing and review boundaries matter more

Do not force multi-repo because it sounds more advanced.
Do not force monorepo because it feels simpler today.
Pick the one that matches ownership and deployment reality.

The rule that keeps the whole thing sane

If you are stuck, ask one question:

Will this boundary make ownership clearer or more confusing six months from now?

That question cuts through a lot of overthinking.

Good Azure DevOps structure is really about:

  • clean ownership
  • clean visibility
  • clean delivery flow
  • clean permission boundaries

If your structure improves those four things, you are probably making the right call.

Final takeaway

The easiest model to remember is this:

  • Organization = company or top-level admin space
  • Project = product, platform, or delivery stream
  • Repo = code boundary

Start there.

Do not over-split on day one, but do not let everything pile into one bucket either.

A clean Azure DevOps structure makes every other part of DevOps easier: pipelines, boards, permissions, onboarding, and ownership.

That is the real W.

References

  1. Microsoft Learn, About organizations, projects, and collections
    https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/about-accounts
  2. Microsoft Learn, Create a project in Azure DevOps
    https://learn.microsoft.com/en-us/azure/devops/organizations/projects/create-project
  3. Microsoft Learn, About Azure Repos
    https://learn.microsoft.com/en-us/azure/devops/repos/get-started/what-is-repos
  4. Microsoft Learn, What is Azure Pipelines?
    https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/what-is-azure-pipelines

A Simple React Hook for localStorage State, Expiry, and Sync

When we build React apps, we often need to save small values in the browser.

For example:

  • selected theme
  • user settings
  • form draft
  • search filters
  • temporary session value
  • cached API response

The browser already gives us localStorage, but using it directly in React can become repetitive.

We usually need to:

  • read the value from localStorage
  • parse the saved JSON
  • handle missing or broken data
  • update React state
  • write the new value back to localStorage
  • remove the value when needed
  • sync the value between tabs
  • expire old values after some time

This is why I built @gks101/localyx.

It is a small React hook that helps you use localStorage like normal React state.

Main Features

  • simple useState-like API
  • saves state in localStorage
  • Same Tab and Cross Tab Sync
  • TTL expiry support
  • absolute and sliding expiry strategies
  • namespace support
  • custom serializer support
  • custom encrypt and decrypt support
  • helper function to check remaining TTL

Working demo:

https://localyx.vercel.app/

GitHub repo:

https://github.com/gaurav101/localyx

NPM package:

npm install @gks101/localyx

What Problem Does It Solve?

React state is simple:

const [value, setValue] = useState("hello");

But normal React state is lost when the page is refreshed.

localStorage can keep data after refresh, but it does not work like React state by default.

So I wanted a hook that feels like useState, but also saves the value in localStorage.

That is the main idea of useLocalStorageState.

Basic Usage

First install the package:

npm install @gks101/localyx

Then import the hook:

import { useLocalStorageState } from "@gks101/localyx";

Now use it inside a component:

import { useLocalStorageState } from "@gks101/localyx";

function ThemeToggle() {
  const [theme, setTheme, clearTheme] = useLocalStorageState("theme", "light");

  return (
    <div>
      <p>Current theme: {theme}</p>

      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle Theme
      </button>

      <button onClick={clearTheme}>
        Reset Theme
      </button>
    </div>
  );
}

This looks very close to useState.

The hook returns three values:

const [state, setState, removeValue] = useLocalStorageState(key, initialValue);

Step 1: The Key

The first value is the localStorage key.

useLocalStorageState("theme", "light");

Here, "theme" is the key.

The hook will save the value in localStorage using this key.

Step 2: The Initial Value

The second value is the default value.

useLocalStorageState("theme", "light");

Here, "light" is the initial value.

The hook will use this value when:

  • nothing is saved in localStorage
  • the saved data is invalid
  • the saved data has expired
  • you remove the value

Step 3: Reading the State

The first returned value is the current state.

const [theme] = useLocalStorageState("theme", "light");

You can show it in your UI:

<p>Current theme: {theme}</p>

When the page refreshes, the hook reads the saved value from localStorage.

Step 4: Updating the State

The second returned value is the setter function.

const [theme, setTheme] = useLocalStorageState("theme", "light");

You can use it like setState:

setTheme("dark");

You can also use the previous value:

setTheme((oldTheme) => oldTheme === "light" ? "dark" : "light");

When you call setTheme, the hook updates React state and also saves the new value in localStorage.

Step 5: Removing the Value

The third returned value removes the saved data.

const [theme, setTheme, clearTheme] = useLocalStorageState("theme", "light");

Use it like this:

clearTheme();

This removes the value from localStorage and sets the state back to the initial value.

TTL: Expire Data After Some Time

Sometimes we do not want to keep data forever.

For example, a session value or cache value should expire after some time.

TTL means “Time To Live”.

You can pass ttl in milliseconds.

import { useLocalStorageState } from "@gks101/localyx";

function SessionExample() {
  const [session, setSession, clearSession] = useLocalStorageState<string | null>(
    "session",
    null,
    {
      ttl: 30 * 60 * 1000,
    }
  );

  return (
    <div>
      <p>{session ?? "No active session"}</p>

      <button onClick={() => setSession("my-session-token")}>
        Login
      </button>

      <button onClick={clearSession}>
        Logout
      </button>
    </div>
  );
}

In this example, the session expires after 30 minutes.

After expiry, the hook removes the old value and returns the initial value.

Absolute TTL

By default, TTL uses the absolute strategy.

This means the timer starts when the value is saved.

Example:

const [token, setToken] = useLocalStorageState("token", null, {
  ttl: 10 * 60 * 1000,
  ttlStrategy: "absolute",
});

If the value is saved at 10:00, it will expire at 10:10.

Even if the user keeps using the app, the value still expires at that time.

This is useful for things like:

  • login session expiry
  • one-time temporary values
  • short-lived cache

Sliding TTL

The hook also supports sliding TTL.

const [searchCache, setSearchCache] = useLocalStorageState("search_cache", "", {
  ttl: 5 * 60 * 1000,
  ttlStrategy: "sliding",
});

Sliding TTL refreshes the timer when the value is read or synced.

This means active data can stay alive while the user is still using it.

This is useful for:

  • active session-like data
  • recently used filters
  • temporary UI cache

Run Code When a Value Expires

You can pass onExpire.

This function runs when the hook finds that the value has expired.

const [token, setToken] = useLocalStorageState("token", null, {
  ttl: 10 * 60 * 1000,
  onExpire: ({ key, value }) => {
    console.log("Expired key:", key);
    console.log("Expired value:", value);
  },
});

This is useful when you want to:

  • show a message
  • logout the user
  • clear related data
  • send an analytics event

Namespaces

Sometimes many features use localStorage.

To avoid key conflicts, you can use a namespace.

const [profile, setProfile] = useLocalStorageState("profile", null, {
  namespace: "my-app",
});

This stores the value using this key:

my-app:profile

Namespaces are useful when:

  • you have many stored values
  • you are building a large app
  • you want to separate app versions
  • you want cleaner localStorage keys

Same Tab and Cross Tab Sync

If you open the same app in two browser tabs, the hook can keep values in sync.

For example:

  1. Open the app in Tab 1.
  2. Open the app in Tab 2.
  3. Change the theme in Tab 1.
  4. Tab 2 gets the new value too.

The hook also handles same-tab sync.

This is useful when two components use the same storage key on the same page.

Get Remaining TTL

The package also exports getRemainingTtl.

This helper tells you how much time is left before a saved value expires.

import { getRemainingTtl } from "@gks101/localyx";

const remainingMs = getRemainingTtl("session", 30 * 60 * 1000);

console.log(remainingMs);

It returns:

  • a number if time is remaining
  • 0 if the value is already expired but not cleaned yet
  • null if the key does not exist or the value has no TTL data

You can also use it with a namespace:

const remainingMs = getRemainingTtl("session", 30 * 60 * 1000, {
  namespace: "my-app",
});

Custom Serialization

By default, the hook uses JSON.

For most React apps, that is enough.

But you can provide your own serializer if you need custom behavior.

const [value, setValue] = useLocalStorageState("custom", "hello", {
  serializer: {
    stringify: (value) => JSON.stringify(value),
    parse: (value) => JSON.parse(value),
  },
});

This is useful when you want more control over how data is saved and loaded.

Encoding and Encryption

By default, the hook stores data with Base64 encoding.

Base64 is only obfuscation. It is not real security.

If you need real security, pass your own encrypt and decrypt functions.

const encrypt = (raw: string) => {
  return btoa(raw);
};

const decrypt = (raw: string) => {
  return atob(raw);
};

const [secret, setSecret] = useLocalStorageState("secret", "hello", {
  encrypt,
  decrypt,
});

If you want to store plain JSON without Base64 encoding, pass null:

const [value, setValue] = useLocalStorageState("plain", "hello", {
  encrypt: null,
  decrypt: null,
});

Important note: do not store highly sensitive data in localStorage unless you fully understand the security risks.

SSR Safe

The hook has guards for browser-only APIs.

This means it can be used in apps that render on the server, like Next.js.

It will not directly access window or localStorage when they are not available.

Where Can You Use This Hook?

You can use this hook in many React projects.

Good use cases:

  • dark mode or theme setting
  • language preference
  • sidebar open or closed state
  • table filters
  • search filters
  • sort order
  • form draft
  • shopping cart for small projects
  • temporary session value
  • API cache with expiry
  • feature flag cache
  • onboarding step progress
  • recently viewed items

Avoid using it for:

  • passwords
  • private tokens without a security plan
  • very large data
  • data that must always be correct on the server

You Can Also Copy the Hook Into Your Project

You do not have to use the npm package if you do not want another dependency.

You can also copy the hook file from the GitHub repo and use it directly in your project.

Repo link:

https://github.com/gaurav101/localyx

This can be useful when:

  • you want full control over the code
  • you want to change the hook for your app
  • you are learning how the hook works
  • you want to avoid adding a package

Full API

Main hook:

useLocalStorageState<T>(key, initialValue, options?)

It returns:

[state, setState, removeValue]

Options:

{
  ttl?: number;
  ttlStrategy?: "absolute" | "sliding";
  namespace?: string;
  onExpire?: (ctx: { key: string; value: T }) => void;
  now?: () => number;
  encrypt?: ((raw: string) => string) | null;
  decrypt?: ((raw: string) => string) | null;
  serializer?: {
    stringify: (value: T) => string;
    parse: (value: string) => T;
  };
}

Helper:

getRemainingTtl(key, ttl, options?)

Final Thoughts

I built @gks101/localyx because I wanted a simple way to use localStorage in React without writing the same logic again and again.

It gives you a useState-like API, but also adds useful features like expiry, sync, namespaces, and custom serialization.

If you are a React beginner, you can start with the basic example first.

Later, when you need expiry or sync, the hook already supports it.

Package:

npm install @gks101/localyx

GitHub:

https://github.com/gaurav101/localyx

I sold you on /scratchpad. Then I migrated to /note.

Note-vs-scratchpad workflow

This is the third post in a series that started with vibe guiding and continued with an efficiency audit. Two months ago I laid out the four-skill loop: /start-issue writes a /scratchpad, /tackle-scratchpad-block executes one step at a time, status flags transition from pending to done, and /finish-issue reads what shipped and writes the PR title + description.

Why I changed my mind

The driver was token consumption. My employer gave each team dashboards of our AI tool usage and I was always the top consumer on mine — fairly high on the department chart too. That’s when I started to suspect it was not what I was building but how I was working with the tools.

/scratchpad‘s JSON step block and /tackle-scratchpad-block chain meant every task paid the same ceremony tax: status flags to transition, JSON to validate, a fenced block to parse. Useful scaffolding for multi-hour work. Overhead I was paying on every session. If my process was the reason I topped the dashboard, revisiting it was the cheapest experiment I could run.

Once I started looking, the redundancy wasn’t only scratchpad-vs-note. The templates the composite skills were generating — what /start-issue, /start-side-quest, /tackle-pr-comment, and /finish-issue wrote into their output documents — carried weight they didn’t need. A few examples from the audit that led to PR 130:

  • ## Files to Modify re-grouped information the Plan steps already named, just organized by file.
  • ## Documentation & Discoverability was a pre-populated checklist that /finish-issue already re-derived systematically at wrap-up time.
  • ## Acceptance Criteria had Claude copy criteria verbatim from the issue body it already had in context.
  • ## Why Split This Out was three hardcoded bullets that matched every side-quest ever created.

Each of these was small on its own. Cumulatively they were a per-session surtax, paid every time a skill fired. Defaulting to /note instead of /scratchpad was the biggest single cut, but the pattern of the change was wider: stop having the LLM write the same information twice, in different shapes, into different documents.

Claude was changing too

The other thing that shifted in those months was Claude itself. When I first wrote about this, the hard stops at every step made sense for the Claude that was around then. By the time PR 130 landed, Claude had gotten noticeably better at multi-step self-organization across an issue. I could lower my guard a bit. The control-freak posture I had started with was no longer needed. If I wanted an explicit checkpoint to manually review and commit, I could simply add a step in the generated /note.

Parallelism shifted too. Claude gained the ability to fan out workers and run agents in parallel within a single session, which meant on issues with independent pieces Claude could organize its own throughput faster than I could shepherd it through the /tackle-scratchpad-block chain. The gates that had once added safety started adding latency. By gating with /tackle-scratchpad-block, I was slowing Claude down by asking for more control.

What /note looks like

A /scratchpad‘s ## Implementation Plan section is a fenced JSON block with status fields, dependency arrays, and task lists:

{
  "finish_issue_on_complete": false,
  "steps": [
    {
      "id": "S001",
      "title": "Swap the token library",
      "status": "pending",
      "done_when": "Old lib removed from package.json, new one imported and passing existing tests",
      "depends_on": [],
      "files": ["package.json", "src/auth/token.ts"],
      "tasks": [
        "npm install new-token-lib, npm remove old-token-lib",
        "Update src/auth/token.ts imports",
        "Run the token test suite"
      ]
    },
    {
      "id": "S002",
      "title": "Update callers to match new token API",
      "status": "pending",
      "depends_on": ["S001"],
      "files": ["src/middleware/auth.ts", "src/routes/login.ts"],
      "tasks": [
        "Update verifyToken() call sites to new API shape",
        "Run full test suite"
      ]
    }
  ]
}

A /note‘s ## Plan section is the same information without the scaffolding:

1. Swap the token library — npm install new-token-lib, update imports, run the token test suite
2. Update callers to match the new token API — verifyToken() call sites, full test suite

No fenced JSON block. No status: pending. The plan says what to do and in what order; the LLM organizes its own execution in-session. The hard stops from the original workflow are still there: I read the plan before saying “go ahead”, I review the diff, I commit when I’m satisfied. The commit lands at the end of the note, not after every step. If I want an earlier checkpoint, I add an explicit gate in the plan.

Note-vs-scratchpad workflow

/scratchpad is still there. You opt in two ways: pass --scratchpad to the invoking skill, or use one of the natural-language triggers the skill watches for (“use a scratchpad”, “with step tracking”, “formal plan”, “track steps”). I still reach for it when I’m working through a larger GitHub issue and want a commit after each step instead of one at the end. This is especially helpful when I’m juggling two or three worktrees on the same project — iterative commits and step tracking keep me oriented when my attention is split across parallel branches.

What I am taking away

The /note flow still stops for plan review before anything happens, then gives the LLM more autonomy to fan out and move faster. The /scratchpad flow is still powerful — but you don’t always need a bazooka to kill a fly. Most days the lighter tool is enough, ending on a high note.

If you already use these skills

Pull the latest from the repo and run ./setup.sh to symlink the updated set. /start-issue will produce a /note on your next invocation. Nothing else changes — the rest of the loop carries on. If you want the old behavior, type --scratchpad.

Written using the same skills it describes, starting from issue #139. The plan was a note this time.

Fixing WSL Errors on Windows 11

Fixing WSL Errors on Windows 11: From Missing LxssManager to WSL2 Migration

Recently, I ran into a frustrating WSL2 issue while setting up my development environment on Windows 11. Everything had been working fine before — Docker, Node.js, npm, and my Linux workflow inside Ubuntu.

Then suddenly, WSL started failing.

At first, it looked like a simple Node.js issue because running:

npm -v

returned:

WSL 1 is not supported. Please upgrade to WSL 2 or above.
Could not determine Node.js install directory

But after checking further on the Windows side, things became much more interesting.

Running:

wsl

returned:

The service did not respond to the start or control request in a timely fashion.
Error code: Wsl/0x8007041d

And then the biggest clue appeared:

Get-Service LxssManager

Output:

Cannot find any service with service name 'LxssManager'.

At that point, it became clear this was not just a Node.js issue. The actual problem was that the WSL service stack itself had become corrupted.

After debugging virtualization, Hyper-V, WSL features, and repairing Windows components, I finally restored WSL properly and migrated the distro fully to WSL2.

This article documents the entire debugging and recovery process so others can fix the same issue faster.

If you’re trying to use Node.js, Docker, or modern development tools inside Windows Subsystem for Linux (WSL) and suddenly encounter errors like:

WSL 1 is not supported. Please upgrade to WSL 2 or above.
Could not determine Node.js install directory

or on Windows:

The service did not respond to the start or control request in a timely fashion.
Error code: Wsl/0x8007041d

this guide walks through how to debug and repair the issue safely on Windows 11.

Understanding the Problem

In this case, there were actually two separate issues:

  1. The WSL service stack on Windows was partially broken.
  2. The Linux distro was still running on WSL1 instead of WSL2.

Modern developer tools such as:

  • Node.js
  • npm
  • Docker Desktop
  • VS Code Remote WSL
  • Kubernetes tooling

work significantly better — and sometimes only work correctly — with WSL2.

Symptoms

Windows-side Errors

Running WSL commands failed with:

wsl

Output:

The service did not respond to the start or control request in a timely fashion.
Error code: Wsl/0x8007041d

Checking the WSL service:

Get-Service LxssManager

Output:

Cannot find any service with service name 'LxssManager'.

This indicated the WSL service registration was corrupted or missing.

Linux-side Errors

Inside Ubuntu:

npm -v

Output:

WSL 1 is not supported. Please upgrade to WSL 2 or above.
Could not determine Node.js install directory

This meant the distro was still using WSL1.

Why This Happens

This usually occurs after one of the following:

  • Interrupted Windows updates
  • Corrupted optional Windows features
  • Broken Docker Desktop installation/removal
  • Hyper-V or virtualization issues
  • Debloating scripts or registry cleaners
  • Failed WSL upgrades

The missing LxssManager service is especially important because it powers the WSL infrastructure.

Step 1: Verify Virtualization

Open PowerShell as Administrator:

systeminfo | findstr /i virtualization

You should see:

Virtualization Enabled In Firmware: Yes

If not:

  • Enter BIOS/UEFI
  • Enable Intel VT-x or AMD-V/SVM
  • Save and reboot

Step 2: Repair Windows Components

Run these commands in Administrator PowerShell:

DISM /Online /Cleanup-Image /RestoreHealth

Then:

sfc /scannow

These commands repair corrupted Windows system files and component registrations.

Reboot after both commands complete.

Step 3: Re-enable WSL Features

Disable broken components first:

dism /online /disable-feature /featurename:Microsoft-Windows-Subsystem-Linux /norestart

dism /online /disable-feature /featurename:VirtualMachinePlatform /norestart

dism /online /disable-feature /featurename:HypervisorPlatform /norestart

Reboot.

Then re-enable them:

dism /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

dism /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

dism /online /enable-feature /featurename:HypervisorPlatform /all /norestart

Enable Hyper-V launch:

bcdedit /set hypervisorlaunchtype auto

Reboot again.

Step 4: Update Windows 11

Go to:

Settings → Windows Update → Check for updates

Install:

  • Cumulative updates
  • Optional updates
  • Feature updates
  • Servicing stack updates

Reboot after installation.

Step 5: Verify WSL Service

Check if the service exists:

Get-Service LxssManager

Expected output:

Status   Name          DisplayName
------   ----          -----------
Running  LxssManager   LxssManager

If the service still does not exist, perform an in-place repair upgrade of Windows 11.

Step 6: In-Place Repair Upgrade (Recommended)

If WSL services are still missing, the cleanest solution is an in-place Windows repair.

Download the Windows 11 installer from Microsoft and choose:

Keep personal files and apps

This repairs:

  • Missing system services
  • Broken Hyper-V registrations
  • Corrupted WSL components
  • Windows component store issues

without deleting personal data.

Step 7: Upgrade from WSL1 to WSL2

Once Windows is repaired, migrate your distro to WSL2.

Check current versions:

wsl -l -v

Example:

NAME      STATE   VERSION
Ubuntu    Running 1

Set WSL2 as default:

wsl --set-default-version 2

Convert the distro:

wsl --set-version Ubuntu 2

Verify again:

wsl -l -v

Expected:

VERSION = 2

Step 8: Verify Node.js and npm

Inside WSL:

node -v
npm -v

Both commands should now work correctly.

Why WSL2 Matters for Developers

WSL2 includes:

  • A real Linux kernel
  • Better filesystem performance
  • Docker compatibility
  • Improved networking
  • Better Node.js compatibility
  • Proper Linux syscall support

For modern web development, DevOps, and container workflows, WSL2 is effectively required.

Recommended Setup After Repair

After fixing WSL:

  • Install Node.js using nvm
  • Use Docker Desktop with WSL2 integration
  • Store projects inside the Linux filesystem (~/projects)
  • Use VS Code Remote WSL extension
  • Keep Windows and WSL updated regularly

Removing Ubuntu from WSL

To remove Ubuntu from WSL on Windows, you unregister the distro. This completely deletes the Linux environment and all associated files.

Step 1 — See Installed WSL Distros

Open PowerShell:

wsl -l -v

Example output:

  NAME      STATE   VERSION
* Ubuntu    Running 2

Step 2 — Stop WSL

Before unregistering the distro, shut down WSL:

wsl --shutdown

Step 3 — Uninstall Ubuntu from WSL

Run:

wsl --unregister Ubuntu

If your distro name is different, use the exact name:

wsl --unregister Ubuntu-22.04

This permanently deletes:

  • Linux files
  • /home directory
  • Installed packages
  • User configuration

This action cannot be undone.

Step 4 — Remove Leftover App (Optional)

If Ubuntu was installed from Microsoft Store:

Settings → Apps → Installed apps

Search for:

Ubuntu

Then click:

Uninstall

Step 5 — Verify Removal

Run:

wsl -l -v

Ubuntu should no longer appear.

Reinstall Ubuntu Cleanly

If your goal is to repair filesystem corruption or reset the environment, reinstall Ubuntu cleanly:

wsl --install -d Ubuntu

WSL2 Disk Space Tip: Enable Sparse VHD Storage

WSL2 stores Linux files inside a virtual disk (ext4.vhdx). Over time this file can grow very large and not automatically shrink.

To reclaim disk space efficiently, newer WSL versions support sparse virtual disks.

Check current WSL version:

wsl --version

To optimize and compact WSL storage:

wsl --shutdown

Then from PowerShell:

Optimize-VHD -Path "$env:LOCALAPPDATAPackages<DISTRO_PACKAGE>LocalStateext4.vhdx" -Mode Full

You may need Hyper-V PowerShell tools enabled.

Another useful command for newer WSL builds:

wsl --manage <distro-name> --set-sparse true

Example:

wsl --manage Ubuntu --set-sparse true

Sparse mode helps reduce unnecessary disk growth and improves storage efficiency for development environments.

Final Thoughts

The key insight during debugging was recognizing that this was not simply a Node.js problem.

The real issue was:

  • a broken WSL service stack on Windows
  • combined with an outdated WSL1 distro

Once the Windows virtualization stack and WSL services were repaired, upgrading to WSL2 resolved the development environment issues completely.

If you encounter missing LxssManager, 0x8007041d, or WSL1 compatibility problems, focus on repairing Windows components first before reinstalling developer tools.

Your app is not Netflix. Stop building like it is.

A developer’s job is to find the simplest solution to a problem. But being simple is actually harder than it sounds.
So why are you over-engineering everything ?

Let me introduce you to Toto.
Toto just finished a great training program. He learned architecture, good practices, microservices, DevOps with Kubernetes, Jenkins, the whole thing. Now Toto has to build an app for a client probably under 1,000 users.

But Toto builds a microservice for every feature. Multiple databases. A massive CI/CD pipeline. He thinks he is doing the right thing. But he is not. He does not need any of that.
Six months later.

Bug in production. 2am. Toto’s mega-architecture is down. He has to debug alone across 12 microservices and his mind keeps saying if only I had kept it simpler. If only I had built an architecture that matched my actual needs.

The message is simple.

Use the architecture and tools you actually need. A solid monolith, Docker, and GitHub Actions will handle 90% of real projects. Microservices are not a sign of skill knowing when NOT to use them is.

Don’t fall into this trap. I almost did. Maybe you will too.
This story is fictional. But you probably know a Toto.

Resolving inter-service communication issue

This week was heck of a roller coaster of emotions, trying to solve an issue. All I faced is the same error again and again. Precisely, error code 307 and 422 that is Temporary Redirect Unprocessable Entity respectively.

Started with a simple notebook for gateway service just like I did for ingestion service at its starting phase. But here is a catch, this time i am dealing gateway service, meaning I need to use uvicorn command here. In case of ingestion service, when I started, all I did is assign a file path to a variable, pass it to PyPDFLoader, split the returned Document into chunks and store them into a storage, called as vector store.

I used uvicorn when I was ready with all the class definitions and folder structures, just to wrap the core script around fastapi and deal with api testing using postman. The functionality that I assigned for gateway is different. As the name suggests, it shall act as a mediator between the client and the backend. For that I need to connect gateway service to ingestion service.

I installed all the necessary libraries in a virtual environment, namely fastapi, uvicorn and httpx, activated the virtual environment. I had an impression that i need use schema validation for the incoming data, convert it to dictionary using .model_dump() and then pass it to httpx.AsyncClient().post(), since i am using post operation.

The post operation has been used because the file shall be uploaded using postman, shall help to store the embeddings in a database. That uploaded file shall be of type UploadFile imported from fastapi library. At the end i stated uvicorn.run() statement to activate my gateway service..

This is where the story begins…

I encountered my first issue in jupiter notebook running a uvicorn statement from where gateway service shall begin to run. I forgot that even jupiter notebook run on a server and since i am running it locally, of course it shall run on localhost.

What I discovered is that while stating on jupyter it is incorrect to use uvicorn statement as:

The reason the first method did helped because a jupyter notebook is not just a text document, it is an active web application. It requires its own constantly running event loop in the background to handle cell executions, process outputs, and communicate with your browser. When I called uvicorn.run(), uvicorn attempts to start a brand new event loop to listen for incoming web requests. Python’s standard asyncio library strictly forbids starting a new event loop while one is already running in the same thread. It throws the RuntimeError to prevent the two managers from fighting over control.

By thread I mean a worker which executes a set of instructions available as a recipe.

instead the following code fragment needs to be used:

A simple screenshot captured from my github repo. The output was previously showcased after running that cell and then commented it out.

Another alternative can by stating the following in a notebook cell :

%%writefile main.py
from fastapi import FastAPI
app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

this shall automatically create a main.py file sibling to the notebook file and the following uvicorn command can be used:

!uvicorn main:app --port 8001 --reload

However my issue has not finished yet. This was just the first part.
The second part is while uploading a pdf in postman, and clicking on Send button, I faced a series on errors one after another.

now even after making some amendments in jupyter notebook, i encountered the error which is shown previously in my codebase. I had to restart the session again and again, may be because of the fact that the previously compiled python script persisted even after making changes. So i shifted to simple .py file which was a better option.

After I was fed up of facing the same error for 2 days, I browsed in google “do i need schema validation using pydantic when a function param of type fastapi.UploadFile, wrapped around the given pydantic model and apply .model_dump() just to be passed in httpx.AsyncClient() ?”

The answer was, only when multiple parameters are passed in the function, in this the function name is forward_to_ingestion, but this time i have only 1 param.

Plus I was mixing things up unncessarily putting .model_dump() after wrapping Around IngestionSchema pydantic class not reallizing that I already put UploadFile at the function header to begin with. I removed the schema defintion then.

At that time i used .model_dump, hoping this would return a json data, but where is the key, i completely forgot that it is simply a variable incoming before which the content within the uploaded file needs to read. At least i did the right thing to use await and .read() for that.

The result is still the same..made me check what mistake did i commit in ingestion service, in an isolated fashion, no inter service communication in this case. Was running fine..

i passed the same snippet to AI, told me to use

tempfile.NamedTemporaryFile

helps to keep memory leakage in check and for security reasons.

meanwhile i also faced 307 error code..specifying /ingest/

I removed the trailing slash in postman, after I crashed into another error

The terminal showed this

One thing i was sure there is something still missing in my gateway service. I passed this snippet:

it simply added follow_redirects=True because of the fact that

httpx does not follow HTTP redirects by default, which is crucial for forwarding files and post data.

I then encountered another problem this time it timeout error, not shown in postman but in terminal.

In postman the same internal server error is shown.

I then specified timeout argument as well.

And I did not believe, it really worked…after seeing this in postman

Yes it is my fault that i should have specified status code as 201 since 200 is the default status code, which does not help in debugging.

But this taught me one thing is to NEVER GIVE UP!!

I built an email cleaner. CSV parsing took longer than the actual validators.

I’ve been building databridge.so by myself for a while. It’s an email list cleaner that explains every decision. Most cleaners give you back “74/100, risky” and that’s all you get. You cannot audit it. So I built one where every row in the cleaned CSV carries the actual reason it was flagged.

A few things I expected to be quick that weren’t.

CSV parsing ate the most time

This is the part I’d skip in any project brief. “Oh, we’ll just use a CSV library.” Then you get a real export from a real CRM and it has:

  • A UTF-8 BOM at the start that breaks header parsing if you don’t strip it
  • Curly quotes from a Google Sheets export instead of straight quotes
  • Mixed line endings (CRLF and LF in the same file, somehow)
  • Ragged rows where the header has 5 columns and some rows have 4 or 6
  • The file is labeled UTF-8 but is actually latin1
  • Commas inside cells without proper escaping
  • A row that contains a literal newline inside a quoted cell

Most cleaners just reject malformed files. I spent weeks getting the parser to repair what’s repairable and clearly flag what isn’t. It is by far the least glamorous part of the codebase and it taught me the most.

“Risky” without a reason is useless

The thing that bothered me most about every cleaner I tested was this. You upload 10,000 emails. You get back 6,200 valid, 2,300 risky, 1,500 invalid. Cool. Now your boss asks “why were these 2,300 flagged?” and the answer is a confidence score with no context.

I wanted the answer to be in the export. So the cleaned CSV has a column for the verdict and a column for the reason, per row. “Domain has no MX.” “Likely typo on gmial.com.” “Role-based address (info@).” Stuff a non technical person can read.

Things that surprised me

A few smaller things worth mentioning:

Typo repair is easy to do too aggressively. Fine to suggest gmial.com becomes gmail.com. Not fine to suggest a random 4-letter domain becomes anything, the false positive rate is too high. I gated repair to longer domains and a small set of high-confidence target providers.

Auto-repair on the file level is way more work than you think. Not just parsing — you have to be confident enough to say “this broken row, here’s what I think it should be” without overwriting actual user data. I lean conservative: repair when there’s one obvious fix, flag otherwise.

Stack

Next.js, Supabase Postgres, Railway workers for the heavy validation, Upstash for queue and cache, Resend for transactional mail. Nothing exotic.

Three open questions

Genuinely curious what people think:

  1. Single deliverability score vs per-check breakdown. A single number flattens signal. bad TLD + role-based is worse than the sum of its parts. But people want one number. I expose both and let the caller pick. Not sure that’s right.

  2. Plus aliases in dedup. [email protected] vs [email protected]. Outreach users want them collapsed, lifecycle people want them separate. I keep them separate by default but get this question a lot.

  3. Async vs sync for small batch jobs. Right now batches are async with a webhook on completion. Some users want a sync response for under 100 rows. Worth supporting both shapes or is that just complexity?

if you want to poke at it. Happy to take pushback in the comments.

databridge.so

How I Would Learn Full-Stack Development in 2026 If I Started From Zero

If I had to start learning full-stack development from zero in 2026, I would not try to learn everything.

That is the biggest mistake many beginners make.

They search for a “full-stack developer roadmap” and suddenly feel like they need to learn:

  • HTML
  • CSS
  • JavaScript
  • TypeScript
  • React
  • Next.js
  • Node.js
  • Databases
  • Docker
  • Testing
  • Cloud hosting
  • System design
  • AI tools
  • A hundred other things

That is overwhelming.

If I started again, my goal would be simple:

Build real applications as early as possible.

Not watch tutorials forever.

Not memorize every framework.

Not wait until I feel ready.

Just build, break things, fix them, and improve.

The Stack I Would Choose

If I started from zero in 2026, I would choose one simple stack and stick with it.

My beginner stack would be:

  • Frontend: HTML, CSS, JavaScript, React
  • Backend: Node.js with Express
  • Database: PostgreSQL
  • Styling: Tailwind CSS
  • Tools: Git, GitHub, VS Code, browser dev tools
  • Deployment: Vercel, Render, Railway, Fly.io, or a simple VPS later
  • AI tools: ChatGPT, Cursor, or Copilot as helpers

I would not try to learn React, Vue, Angular, Svelte, and Solid at the same time.

I would not start with Kubernetes.

I would not start with microservices.

I would not try to become an expert in everything.

The goal is not to choose the perfect stack.

The goal is to choose a stack that helps you build real projects.

Phase 1: Learn the Basics of the Web

I would start with the basics:

  • HTML
  • CSS
  • JavaScript
  • Responsive layouts
  • Forms
  • Buttons
  • Inputs
  • Flexbox
  • Grid
  • DOM manipulation
  • Fetch API
  • Async/await

This stage does not need to take forever, but it is important.

If you skip the basics, everything later becomes harder.

For example:

  • If you do not understand HTML forms, React forms feel confusing.
  • If you do not understand CSS layout, Tailwind feels random.
  • If you do not understand JavaScript arrays and objects, API responses feel difficult.

At this stage, I would build small projects like:

  • A landing page
  • A pricing page
  • A calculator
  • A todo list
  • A simple weather app
  • A responsive portfolio homepage

These projects may seem basic, but they teach the foundation that everything else depends on.

Phase 2: Learn Git Early

I would learn Git early.

Not advanced Git.

Just the basics:

  • git init
  • git status
  • git add
  • git commit
  • git push
  • Branches
  • Pull requests
  • Basic merge conflicts

Then I would upload every project to GitHub.

This gives you two benefits:

  • You learn version control.
  • You build proof that you are actually creating things.

A messy GitHub with real projects is better than an empty GitHub with perfect intentions.

Phase 3: Learn React by Building Interfaces

After the basics, I would learn React.

But I would not watch a huge course passively.

I would learn one concept, then build something with it.

For example:

  • Components: build cards and sections
  • Props: build reusable buttons and profile cards
  • State: build counters, modals, tabs, and forms
  • Effects: fetch data from an API
  • Routing: build a multi-page app
  • Forms: build login and signup screens

At the beginning, I would avoid:

  • Redux
  • Complex state libraries
  • Advanced patterns
  • Overengineering

The beginner goal is simple:

Can I build a useful interface?

Can I pass data around?

Can I fetch data?

Can I handle loading and error states?

Can I make the app work on mobile?

Once I can do that, I would recreate common app screens:

  • Dashboard
  • Settings page
  • User profile
  • Login page
  • Blog homepage
  • Product listing page
  • Checkout-style form

This is where you start thinking like a frontend developer.

Phase 4: Learn Backend Basics

Once I can build frontend apps, I would learn backend development.

I would start with Node.js and Express because it is simple and teaches the core ideas.

I would learn:

  • HTTP methods
  • Routes
  • Controllers
  • Request and response
  • Middleware
  • Validation
  • Error handling
  • Authentication basics
  • Password hashing
  • REST API design
  • Database queries

Then I would build small APIs:

  • Notes API
  • Blog API
  • Habit tracker API
  • Bookmark manager API
  • Simple user authentication system

At this point, the most important thing is understanding the full request-response loop:

  • A user clicks a button.
  • The frontend sends a request.
  • The backend receives it.
  • The backend validates it.
  • The backend talks to the database.
  • The backend returns data.
  • The frontend updates the UI.

That loop is the heart of full-stack development.

Once you understand that, full-stack development becomes much less mysterious.

Phase 5: Learn SQL and PostgreSQL

I would learn SQL earlier than many beginners do.

A lot of beginners start with MongoDB because it feels easier. MongoDB can be useful, but I would personally choose PostgreSQL as my main beginner database.

Why?

Because SQL teaches you how data relates.

Real applications often have relationships like:

  • Users have posts.
  • Posts have comments.
  • Orders have products.
  • Students have lessons.
  • Mentors have bookings.

I would learn:

  • Tables
  • Rows
  • Columns
  • Primary keys
  • Foreign keys
  • One-to-many relationships
  • Many-to-many relationships
  • Joins
  • Migrations
  • Basic database design

Then I would use an ORM like Prisma, Drizzle, or TypeORM.

But only after I understand the basics of SQL.

The mistake is not using an ORM.

The mistake is using an ORM without understanding what it does.

Phase 6: Build Real Full-Stack Projects

This is where everything comes together.

I would not build random toy projects forever.

I would build projects that feel close to real products.

For example:

  • A task management app
  • A blog CMS
  • A booking app
  • A feedback platform
  • A small SaaS dashboard
  • A habit tracking app
  • A simple marketplace

These projects teach important full-stack concepts:

  • Authentication
  • Permissions
  • Database structure
  • API design
  • Frontend state
  • Deployment
  • Error handling
  • User experience

This is where you stop just learning syntax and start learning how software actually works.

Phase 7: Deploy Everything

A project is not finished when it works on your laptop.

A project is finished when someone else can open it in a browser and use it.

So I would deploy every serious project.

Deployment teaches real-world lessons:

  • Environment variables break.
  • Databases need connection strings.
  • CORS errors happen.
  • Build commands fail.
  • APIs need production URLs.
  • Logs become important.
  • Performance starts to matter.

This stage can be frustrating, but it is extremely valuable.

You become a better developer when you ship things.

Phase 8: Add TypeScript

I would not start with TypeScript on day one.

But I also would not wait too long.

Once JavaScript, React, and API communication make sense, I would add TypeScript.

I would learn:

  • Basic types
  • Interfaces
  • Type aliases
  • Union types
  • Optional properties
  • Function types
  • Typing React props
  • Typing API responses
  • Basic generics

The goal is not to become a TypeScript expert immediately.

The goal is to write safer code and understand professional codebases more easily.

Phase 9: Use AI, But Do Not Let It Think For You

Learning full-stack development in 2026 without AI would be unnecessary.

AI tools can help a lot.

They can:

  • Explain errors
  • Generate small examples
  • Review your code
  • Suggest improvements
  • Explain unfamiliar syntax
  • Help with boilerplate
  • Quiz you on concepts

But there is a danger.

If AI writes everything and you do not understand it, you are not learning.

You are just collecting code you cannot maintain.

My rule would be:

If AI writes it, I need to understand it before I move on.

Use AI as a mentor.

Do not use it as a replacement for your brain.

Phase 10: Learn Debugging

Most beginners think coding is mainly about writing code.

But real development is often about debugging.

I would intentionally practice:

  • Reading error messages
  • Using console.log properly
  • Using browser dev tools
  • Inspecting network requests
  • Checking response codes
  • Reading backend logs
  • Searching documentation
  • Reproducing bugs
  • Isolating problems

Debugging is one of the most valuable skills you can build.

The better you get at debugging, the faster you learn everything else.

My 6-Month Beginner Roadmap

If I had six months to learn full-stack development from zero, I would structure it like this:

Month 1: Web Basics

  • HTML
  • CSS
  • JavaScript
  • Git
  • Responsive layouts
  • Small projects

Month 2: React

  • Components
  • Props
  • State
  • Effects
  • Routing
  • Forms
  • API calls

Month 3: Backend Basics

  • Node.js
  • Express
  • REST APIs
  • Validation
  • Error handling
  • Authentication

Month 4: Databases

  • PostgreSQL
  • SQL basics
  • Relationships
  • Migrations
  • ORM basics

Month 5: Full-Stack Projects

  • Build two serious projects
  • Add authentication
  • Connect frontend and backend
  • Use a database
  • Deploy the apps

Month 6: Polish and Portfolio

  • Add TypeScript
  • Improve UI
  • Refactor code
  • Add basic tests
  • Write project case studies
  • Build a simple portfolio

This would not make you a senior developer.

But it would give you a strong foundation.

More importantly, it would give you proof that you can build real things.

What I Would Avoid

If I started again, I would avoid:

  • Watching tutorials without building
  • Changing stacks every two weeks
  • Trying to learn everything at once
  • Copy-pasting AI code without understanding it
  • Avoiding deployment
  • Learning advanced architecture too early
  • Comparing myself to experienced developers
  • Waiting until I feel ready

You do not become ready first.

You become ready by building.

Final Thoughts

Full-stack development can look intimidating because there are many layers:

  • Frontend
  • Backend
  • Database
  • Authentication
  • APIs
  • Deployment
  • Security
  • Testing
  • Performance

But you do not need to master all of them before you start.

You need to start small.

Build something.

Break something.

Fix it.

Repeat.

That is how you learn.

The best way to learn full-stack development in 2026 is not to collect more tutorials.

It is to build increasingly realistic applications until the pieces start connecting in your head.

Start simple.

Stay consistent.

Deploy your work.

Use AI wisely.

And most importantly:

Do not wait until you understand everything.

You never will.

That is part of the job.

Want help learning full-stack development with a clear roadmap, real projects, and personal guidance? Check out my Full-Stack Developer Mentoring.