惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Hacker News
The Hacker News
Martin Fowler
Martin Fowler
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
U
Unit 42
F
Full Disclosure
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recorded Future
Recorded Future
Security Archives - TechRepublic
Security Archives - TechRepublic
阮一峰的网络日志
阮一峰的网络日志
T
Threatpost
P
Privacy International News Feed
GbyAI
GbyAI
Stack Overflow Blog
Stack Overflow Blog
MongoDB | Blog
MongoDB | Blog
I
Intezer
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Privacy & Cybersecurity Law Blog
A
Arctic Wolf
博客园 - 聂微东
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
H
Help Net Security
S
Schneier on Security
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
The Exploit Database - CXSecurity.com
T
Tor Project blog
月光博客
月光博客
NISL@THU
NISL@THU
A
About on SuperTechFans
Spread Privacy
Spread Privacy
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
G
Google Developers Blog
W
WeLiveSecurity
P
Palo Alto Networks Blog
The Last Watchdog
The Last Watchdog
K
Kaspersky official blog
博客园 - 司徒正美
L
LINUX DO - 热门话题
小众软件
小众软件

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building a Local-First Task Manager with Next.js and React — Lessons from Planow
Yogesh Raj K · 2026-04-30 · via DEV Community

An honest engineering breakdown of building Planow — the decisions, tradeoffs, and lessons that mattered.

Syed and I had been circling around the idea of building Planow for roughly four years. Not continuously — it surfaced the way most side projects do: in moments of frustration, when existing tools felt unnecessarily complex or slow.

We didn’t want another bloated productivity tool.

We wanted something that answers one question instantly:

“What should I do right now?”

That naturally led us to the Eisenhower Matrix.

The idea was simple:

  • Categorize tasks by urgency and importance
  • Make prioritization visual and immediate

But building something that feels instant and intuitive turned out to be the real challenge.

Planow is built on a simple idea: your tasks should be fast, private, and available the moment you think of them — not after a network request.


The Stack — Choosing the Future (With Tradeoffs)

I made a deliberate decision early: Build on where the ecosystem is going, not where it is.

  • Next.js 16 (Canary) + React 19
  • TypeScript
  • Tailwind CSS v4
  • Zustand
  • Supabase
  • @hello-pangea/dnd

Next.js 16 (Canary) + React 19: Both pre-stable when I started. I made this call deliberately. My reasoning: we were building something we intended to use long-term, and building on the current stable release meant I’d be doing a framework migration in six months regardless. React 19’s improvements to concurrent rendering and transitions were directly relevant to the drag-and-drop experience I had in mind. I accepted the tradeoff — occasional canary instability, fewer Stack Overflow answers — in exchange for building on where the ecosystem was actually heading.

TypeScript throughout: Not a stylistic preference. The core Task interface flows through the UI layer, the Zustand store, the Supabase integration, and the drag-and-drop logic. Without TypeScript keeping the shape of that data honest across all four, you’re just hoping nothing breaks at runtime.

Tailwind CSS v4 — also pre-stable. The removal of the tailwind.config.js requirement and improved CSS variable integration in v4 were what pushed me toward it. I use Tailwind for layout utilities, spacing, and responsive breakpoints only. All color and typography tokens live in theme.css as CSS custom properties. That architecture decision mattered more than the Tailwind version.

Zustand for state. Redux felt excessive for the scope. Context didn’t scale cleanly. Zustand hit the right balance. Zustand was minimal and powerful. Zustand worked because of its simple API, centralized state, easy async handling and built-in persistence support. It became the backbone for state, persistence, and optimistic updates.

Supabase for backend, auth, and real-time. PostgreSQL, row-level security, Google OAuth, and WebSocket subscriptions in one managed service. I’m not in the business of running database infrastructure. Supabase let me stay focused on the product.

@hello-pangea/dnd for drag-and-drop. The community-maintained fork of react-beautiful-dnd, updated for React 18+ compatibility. react-beautiful-dnd is no longer actively maintained by Atlassian — using the fork was the safer long-term call.


The Data Architecture — Getting Flat Right

Early in the build I had to make a foundational decision about the data shape. The obvious approach for a Kanban-style board is a map keyed by column name:

// The tempting approach
mainData: {
  Do: Task[],
  Schedule: Task[],
  Delegate: Task[],
  Limit: Task[],
  Later: Task[]
}

Enter fullscreen mode Exit fullscreen mode

This makes rendering easy. It makes drag-and-drop operations locally easy. It makes syncing to a relational database a nightmare. Every operation that touches the database requires you to reconstruct which array a task lives in, and your data shape in the client diverges from your data shape in the DB.

I went with a flat array instead, with each task carrying its own boardName (and later board_id). The rendering layer groups by quadrant. Every database operation is a straightforward row operation.

// Flat array - what I actually used
mainData: Task[] // each task has boardName: 'Do' | 'Schedule' | etc.

Enter fullscreen mode Exit fullscreen mode

This decision paid off when I connected to Supabase. No transformation layer needed. Fetch from DB, set in store, render.


The Drag-and-Drop Reorder Problem

This is where most Kanban implementations quietly take shortcuts.

The naive approach is to sort by createdAt timestamp and treat position as implicit. This works until someone drags a card, at which point your timestamp is semantically wrong. You need an explicit position integer column.

My drag handler:

function handleOnDragEnd(result: any) {
  const { source, destination } = result;
  if (!destination) return;
  const sourceItems = mainData
    .filter((task) => task.boardName === source.droppableId)
    .sort((a, b) => a.position - b.position);
  const destItems = mainData
    .filter((task) => task.boardName === destination.droppableId)
    .sort((a, b) => a.position - b.position);
  if (source.droppableId === destination.droppableId) {
    const reordered = [...sourceItems];
    const [removed] = reordered.splice(source.index, 1);
    reordered.splice(destination.index, 0, removed);
    const updated = reordered.map((task, i) => ({ ...task, position: i }));
    updateData(updated);
    return;
  }
  const newSource = [...sourceItems];
  const [removed] = newSource.splice(source.index, 1);
  const newDest = [...destItems];
  newDest.splice(destination.index, 0, removed);
  const updatedSource = newSource.map((task, i) => ({ ...task, position: i }));
  const updatedDest = newDest.map((task, i) => ({
    ...task,
    boardName: destination.droppableId,
    position: i,
  }));
  const untouched = mainData.filter(
    (t) =>
      t.boardName !== source.droppableId &&
      t.boardName !== destination.droppableId,
  );
  updateData([...untouched, ...updatedSource, ...updatedDest]);
}

Enter fullscreen mode Exit fullscreen mode

On the database side, I needed atomic bulk updates for position changes. A single UPDATE per task would mean N round trips per drag operation. I wrote a Supabase RPC function instead:

create 
or replace function reorder_tasks(payload jsonb) returns setof tasks as $$ begin perform pg_advisory_xact_lock(123456);
return query 
with 
  rows as (
    select 
      (elem ->> 'id'):: uuid as id, 
      (elem ->> 'position'):: int as position, 
      nullif(elem ->> 'board_id', ''):: int as board_id, 
      (elem ->> 'updated_at'):: timestamptz as updated_at 
    from 
      jsonb_array_elements(payload) elem
  ) 
update 
  tasks t 
set 
  position = r.position, 
  board_id = coalesce(r.board_id, t.board_id), 
  updated_at = r.updated_at 
from 
  rows r 
where 
  t.id = r.id 
returning 
  t.*;
end;
$$ language plpgsql;

Enter fullscreen mode Exit fullscreen mode

The pg_advisory_xact_lock is important. Without serializing reorder operations at the transaction level, two concurrent drags from two devices produce a race condition where both writes succeed but the resulting order is wrong. The advisory lock is session-scoped and cheap — it doesn’t block reads, only concurrent writes to the same logical lock ID.


The Local-First Architecture

This is the part I’m most satisfied with, and also the part that took the most iteration to get right.

Planow is designed as a local-first system — tasks are stored locally and work instantly. Cloud sync is optional and enabled only when users choose to log in.

If a user signs in, their locally-created tasks sync to the cloud. If they’re already a cloud user, we fetch from the DB on login. Conflict resolution happens automatically.

The Zustand pattern I landed on:

addTask: async (task) => {
  const previousData = get().mainData;
  const userData = get().userData;
  // Optimistic update - UI responds immediately
  const tempTask = {
    ...task,
    id: crypto.randomUUID(),
    completed: false,
    source: userData ? "remote" : "local",
    updated_at: new Date().toISOString(),
  };
  set((state) => ({ mainData: [...state.mainData, tempTask] }));
  if (userData) {
    const { data, error } = await supabase
      .from("tasks")
      .insert(task)
      .select()
      .single();
    if (error || !data) {
      // Rollback on failure
      set({
        mainData: previousData,
        snackbar: { show: true, content: error?.message, type: "error" },
      });
      return;
    }
    // Replace temp record with real DB record (correct ID)
    set((state) => ({
      mainData: state.mainData.map((t) => (t.id === tempTask.id ? data : t)),
    }));
  }
  // If no userData: stays in local state with source: 'local'
};

Enter fullscreen mode Exit fullscreen mode

The source: ‘local’ marker is how the sync layer knows what to push on login. We only sync tasks carrying that marker — not everything. This prevents unnecessary writes and handles the common case of a user editing tasks while logged out and then logging back in.

The sync RPC handles conflict resolution via updated_at comparison:

create 
or replace function sync_tasks(payload jsonb) returns setof tasks as $$ begin return query insert into tasks (
  id, board_id, completed, position, 
  title, updated_at
) 
select 
  (t ->> 'id'):: uuid, 
  (t ->> 'board_id'):: int, 
  (t ->> 'completed'):: boolean, 
  (t ->> 'position'):: int, 
  t ->> 'title', 
  (t ->> 'updated_at'):: timestamptz 
from 
  jsonb_array_elements(payload) t on conflict (id) do 
update 
set 
  board_id = excluded.board_id, 
  completed = excluded.completed, 
  position = excluded.position, 
  title = excluded.title, 
  updated_at = excluded.updated_at 
where 
  excluded.updated_at > tasks.updated_at 
returning 
  *;
end;
$$ language plpgsql;

Enter fullscreen mode Exit fullscreen mode

Last-write-wins by timestamp. Simple, predictable, and correct for a single-user task manager.


The Privacy Philosophy — Your Data Stays Yours

One principle guided many of our decisions:

Your data belongs to you — not the platform.

Planow is built to work without requiring an account. Tasks are stored locally by default, meaning:

  • No forced sign-ups
  • No automatic cloud sync
  • No hidden data collection

If a user chooses to log in, only then is data synced to the cloud for cross-device access.

This flips the typical model:
Most apps start with the cloud and store data locally as a cache.
Planow starts local — and uses the cloud only when needed.


The Design System — Syed’s Domain, My Implementation

I should be honest here: the design system architecture was a collaborative decision, but Syed drove the visual language. What I implemented was a full CSS custom property token system where every single color in the application is a variable reference. Not one hardcoded hex value in any component.

:root[data-theme="light"] {
 - primary: #5A67FA;
 - background: #F7F7FF;
 - quadrant-do-bg: color-mix(in srgb, #CDFDDD 40%, transparent);
 - quadrant-do-header: #0D834C;
 - quadrant-do-border: #0D834C;
}
:root[data-theme="dark"] {
 - primary: #5A67FA;
 - background: #06060F;
 - quadrant-do-bg: color-mix(in srgb, #0D834C 20%, transparent);
}

Enter fullscreen mode Exit fullscreen mode

The practical consequence: dark mode isn’t a pile of dark: Tailwind overrides. It’s a complete, independently tuned set of token values. Switching themes means flipping the data-theme attribute on . Everything responds. We built out a full opacity ladder for each quadrant color (4%, 8%, 10%, 16%, 32%, 48%, 64%, 80%, 92%) for state layers. Over 400 variables total across both themes. It sounds like overkill for a side project, and maybe it is — but we never had a “this doesn’t look right in dark mode” bug.


What I Got Right

  • Flat data model → simplified everything
  • RPC-based bulk updates → performance win
  • Optimistic UI → better UX
  • CSS token system → scalable theming
  • Zustand → minimal and effective

What Was Harder Than Expected

Drag reorder logic

  • Same column
  • Cross column
  • Position recalculation

Database sync consistency

  • Order remains correct
  • No race conditions

What Planow Is Today

  • A fast, visual task manager
  • Built around the Eisenhower Matrix
  • Real-time synced (when logged in)
  • Optimistic and responsive
  • Clean, minimal UI
  • Dark/light theme supported

What’s Next

  • Collaboration features (data model ready)
  • Reports/analytics UI
  • Improved sync strategies
  • Better onboarding & landing experience

Privacy-focused sync alternatives:

  • Import / Export tasks between devices
  • Manual data transfer without requiring login
  • Full user control over where data lives

The goal is simple:
Users should be able to move their data across devices without being forced into an account system.


The Real Lesson

None of this was “hard” individually.

But:

  • Every small decision compounded
  • Every edge case mattered
  • Every shortcut showed up later

The biggest wins came from:

aligning product decisions with technical architecture early


The Honest Part

Four years from “we should build this” to shipped MVP. The idea was simple. The engineering was not hard exactly — but it was full of small decisions that compounded, and getting each one right took longer than the individual decision seemed to warrant.

The local-first sync architecture, the drag-and-drop position management, the conflict resolution — each of these problems is well-understood in the abstract. Actually implementing them in a way that handles real edge cases (concurrent drags, multi-device sync, auth state changes mid-session) requires you to think through every failure mode and decide how to handle it.

Syed handled product and design. I handled engineering. The best decisions came from the gap between those two functions — the conversations about what the right tradeoff was between implementation complexity and correct behavior.

That’s worth saying plainly:

the most important engineering decisions on this project weren’t purely technical. They were answers to product questions that happened to have technical consequences.


Not a developer? Want to understand what Planow is actually for and why the Eisenhower Matrix might change the way you work? Read the story here: I Stopped Drowning in To-Do Lists the Day I Asked Myself Two Questions

Written by Yogesh Raj · Built with Syed Shahab

Try 👉 https://planow.app/ and let me know your honest feedback