The Next DEV Weekend Challenge Launches on April 16 – 20. Mark Your Calendar!

The DEV Team logo

We’re back with another installment of our newest format: the DEV Weekend Challenge!

If you missed the first one, these are short-form, high-energy challenges designed to fit right into your weekend. We’re giving you the heads-up now so you can clear your schedule!

How It Works

Our challenge prompt will be revealed at launch. You can keep an eye on the DEV Weekend Challenge page or look out for the official announcement post from the DEV Team. From there, you’ll have the entire weekend to build, document, and submit your project. That’s all there is to it!

Because our community spans every timezone on the planet, we’ve set the window so that everyone around the world gets at least a full weekend to participate.

Important Dates

  • Launch Time: Friday, April 16 at 2:00 AM UTC
  • Submissions Due: Monday, April 20 at 6:59 AM UTC

Here’s what that looks like across a few timezones:

Timezone Launch Time (Local) Submissions Due (Local)
PDT Thursday, Apr 16 at 7:00 PM Sunday, Apr 19 at 11:59 PM
EDT Thursday, Apr 16 at 10:00 PM Monday, Apr 20 at 2:59 AM
GMT Friday, Apr 17 at 2:00 AM Monday, Apr 20 at 6:59 AM
CEST Friday, Apr 17 at 4:00 AM Monday, Apr 20 at 8:59 AM
IST Friday, Apr 17 at 7:30 AM Monday, Apr 20 at 12:29 PM
JST Friday, Apr 17 at 11:00 AM Monday, Apr 20 at 3:59 PM
AEST Friday, Apr 17 at 12:00 PM Monday, Apr 20 at 4:59 PM

While the window technically spans more than 48 hours, our goal is to ensure everyone has a full, uninterrupted weekend to work on their project regardless of where they live.

Can’t wait?

Join the Notion MCP Challenge happening now through March 29!

Also, don’t miss your chance to enter the 2026 WeCoded Challenge, in our writing category “Echoes of Experience” or the brand new “Frontend Art” prompt!

Mark your calendars!

We can’t wait to see what you build!

I built a SaaS to measure AI visibility (GEO) — here’s how it works

AI is quickly becoming a new search layer.

People don’t just Google anymore — they ask ChatGPT, Claude, Gemini…

But here’s the problem:
Companies have no idea if they are visible in these answers.

So I built a tool to solve that.

Introducing GeoTracker

GeoTracker is a web app that analyzes how an AI perceives your company.

It answers questions like:

  • Is my company mentioned in AI responses?
  • Which competitors are shown instead?
  • What sources are used by the AI?
  • How can I improve my visibility?

How it works

The idea is simple:

  1. Generate real-world prompts (like users would ask)
  2. Send them to multiple AI models
  3. Analyze the responses
  4. Extract:
    • brand mentions
    • competitors
    • cited sources
  5. Generate actionable recommendations

Core features

  • Multi-model AI analysis (OpenAI, etc.)
  • Manual prompt testing
  • Batch testing (run multiple prompts at once)
  • Brand & competitor detection
  • Source extraction
  • GEO recommendations engine
  • PDF export for client reports

Tech stack

  • Frontend: React + Vite
  • Backend: FastAPI
  • Database: PostgreSQL
  • Infra: Docker + Nginx (VPS)
  • Auth: Secure cookie (JWT)

Why this matters

We are entering a new era of search.

SEO is no longer enough.

We now need to optimize for AI-generated answers:
GEO (Generative Engine Optimization)

If your company is not mentioned in AI responses, you are invisible.

Example use case

Prompt:

“Best suppliers of slip rings in aerospace”

Result:

  • Your company might not appear
  • Competitors dominate
  • Sources come from specific domains

That’s actionable data.

What’s next

I’m currently working on:

  • automated monitoring
  • scoring & trends
  • multi-client SaaS version
  • better recommendations engine

Final thoughts

This project started as an experiment, but it quickly turned into something bigger.

AI visibility is going to be a major challenge for companies in the next years.

And we’re just at the beginning.

If you’re working on SEO, AI, or similar tools, I’d love to connect

“I Wasted Months on Wrong Skills — Here’s What Actually Gets You Hired in 2026”

“I know — you’ve been working hard. Just maybe in the wrong direction. But hey, that ends today.”

Nobody wants to say it. So I will. 👇

Every year someone posts a “top skills to learn” list. Same advice, different year. Learn React. Learn Python. Learn cloud.

Fine. But nobody talks about the other side — the skills quietly becoming worthless while beginners spend months on them.

This is that post. No sugarcoating. ☕

💀 Skills That Are Dying (or Already Dead)

1. jQuery

jQuery was revolutionary… in 2010.

In 2026, it’s legacy code you’ll encounter in old codebases — not something to actively learn. Modern JavaScript does everything jQuery ever did:

// jQuery way (old)
$('.btn').on('click', () => { ... })

// Vanilla JS (modern)
document.querySelector('.btn').addEventListener('click', () => { ... })

Brutal truth: If jQuery is your main skill, you’re optimizing for maintaining 10-year-old projects — not getting hired anywhere building something new.

2. HTML/CSS Alone — Without JavaScript

“I know HTML and CSS” used to be a valid entry point.

In 2026, it gets you exactly one role: static site builder — and even that’s being eaten alive by AI tools like Framer and Webflow.

HTML and CSS are absolutely essential. But they’re the foundation, not the destination. If you stop there, you stop before the job market begins.

Brutal truth: No frontend job posting in 2026 says “JavaScript optional.” JS is the job. Period.

3. Basic WordPress Development

Building simple WordPress sites used to be a freelancer’s bread and butter. 🍞

Now? AI site builders — Framer AI, Wix AI, even ChatGPT — generate a decent business website in minutes.

Advanced WordPress (custom plugins, headless WP with React) still has a market. Basic theme installation does not.

Brutal truth: You’re entering a market that AI is actively pricing you out of.

4. Memorizing Syntax 🧠

Controversial one. Hear me out.

Spending hours memorizing JavaScript array methods or CSS property names is a poor investment in 2026. GitHub Copilot, ChatGPT, and Claude autocomplete all of it. Every senior dev already has multiple AI tools open while coding.

What can’t be automated? Understanding why something works. When to use it. How to debug it when it breaks.

Brutal truth: The dev who understands concepts deeply but uses AI for syntax will always beat the dev who memorized syntax but doesn’t understand the logic underneath.

5. Random CSS Hacks Without Understanding Layout 🤯

Googling random CSS fixes. Copy-pasting Stack Overflow snippets. Not knowing why z-index isn’t working.

This approach gets more painful every year. Tailwind CSS has standardized frontend styling. CSS Grid and Flexbox made layout predictable.

Brutal truth: Companies don’t want devs who copy-paste CSS blindly. They want devs who understand the box model, the cascade, and layout well enough to write it confidently.

🚀 Skills That Are Rising (Learn These NOW)

1. React + TypeScript ⚛️

This is the industry standard for frontend in 2026. Open any junior frontend job listing — you’ll see React. TypeScript has gone from “nice to have” to flat-out expected.

You don’t need to master TypeScript immediately. Even basic types and interfaces will put you ahead of beginners still writing plain JavaScript.

// TypeScript — simple but powerful
interface User {
  name: string;
  age: number;
  isLoggedIn: boolean;
}

Honest path: Learn React well first. Add TypeScript once you’re comfortable with components and state.

2. AI Tool Integration 🤖

This is the skill with the highest upside for a frontend developer right now.

Companies at every level — startups, agencies, enterprise — are scrambling to add AI features to their products. A developer who can integrate an AI API, handle streaming responses, and build a chat interface is genuinely valuable.

The technical barrier is lower than you think. If you can call a REST API in JavaScript, you can integrate an AI API.

const response = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: prompt }]
  })
})

Honest path: Build one AI-powered tool as a portfolio project. It immediately separates you from 90% of beginners.

3. Git + CI/CD Basics 🔧

Git is non-negotiable. Every team, every company, every freelance project uses version control.

CI/CD — even basic knowledge of how GitHub Actions works, how code auto-deploys — shows you think like a professional developer, not a hobbyist.

# GitHub Actions — auto deploy on push
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm install && npm run build

Honest path: Use Git for every project, even solo ones. Set up a simple GitHub Actions workflow. That alone is resume-worthy.

4. System Design Thinking 🏗️

“Isn’t that for senior engineers?”

Nope. In 2026, even junior interviews at decent companies include basic system design questions. Not “design Twitter at scale” — but:

  • How would you structure this feature?
  • Why would you use a database here?
  • What happens when this API fails?

This is just thinking about your code as a system rather than a collection of files.

Honest path: When you build projects, ask yourself: what if 1000 users used this? What breaks first? That mindset IS system design at the junior level.

5. Next.js + Full-Stack Thinking ⚡

Next.js is the dominant React framework for a reason — it handles routing, server-side rendering, API routes, and deployment in one package.

More importantly, it pushes you to think full-stack. You start understanding where data comes from, how APIs work, what the server is doing — all critical for growing beyond junior level.

Honest path: Once you’re solid in React, your next project should be a Next.js app deployed on Vercel. It’s the fastest way to level up your thinking.

🎯 The Real Skill Nobody Lists

Every list talks about frameworks and tools. Nobody talks about the actual meta-skill:

Learning speed. 🏃

The developer who learns React in a month beats the one spending a year on jQuery. The developer who ships imperfect projects and iterates beats the one waiting to feel “ready.”

Technologies keep changing. The skill that never becomes obsolete is the ability to pick up new things fast, build something with them, and move on.

📊 Quick Summary

💀 Dying 🚀 Rising
jQuery React + TypeScript
HTML/CSS only AI API integration
Basic WordPress Git + CI/CD
Memorizing syntax System design thinking
Random CSS hacks Next.js + full-stack

💬 Your Turn

Disagree with anything on this list? Drop a comment — genuinely curious what you’d add or remove.

And if you’re just starting out and feeling overwhelmed by all this — don’t be. You don’t need to learn everything at once. Pick one “Rising” skill, go deep on it, and ship something. That’s the whole game. 🎮

Found this useful? Drop a ❤️ — it helps more beginners find this post!

Your Financial Data Should Live on Your Device. Here Is the Architecture That Makes That Possible.

How a carefully layered architecture delivers instant offline access, end-to-end privacy, and optional cloud sync, without asking you to choose between them.

Estimated reading time: 10 minutes

The Privacy Paradox of Financial Apps

Every financial app on the market asks you the same question on page one: “Create an account.” Before you can track a single expense, you hand over an email address. Before you can set a savings goal, your data lands on someone else’s server. You are trusting that company’s security practices, their terms of service, their business model, and their continued existence — all to do basic arithmetic with your own money.

Most apps treat this as inevitable. A login wall is the cost of admission. Your data goes to their database. You hope for the best.

Talliofi refuses that deal. Your data never leaves your device unless you explicitly ask it to. No account. No telemetry. No analytics. If you want cloud sync across devices, you bring your own Supabase instance. If you do not, everything stays local. Forever.

This sounds simple in a pitch deck. In code, it requires an architecture that most web apps never need to consider. This article is a complete walkthrough of how we built it.

What “Local-First” Actually Means

The local-first software movement, articulated by Ink & Switch in 2019, makes one core argument: the best software keeps your data on your device and treats sync as a convenience, not a requirement. The app works on a plane. It works when your connection drops mid-session. It works ten years from now, even if the company behind it disappears.

For Talliofi, local-first is not a philosophy; it is a set of concrete engineering constraints:

  • No login required to use the full app. Open it, enter data, close the tab, come back next month. Everything is there.
  • The device is the source of truth. IndexedDB, not a server, holds the canonical state.
  • Sync is additive, not required. The app does not degrade without it.

This inverts the standard web app model. In a typical application, the server is the source of truth and the client is a view layer. In Talliofi, the client is the source of truth. The server, if present at all, is a backup.

Layer 1: IndexedDB as the Source of Truth

Talliofi uses Dexie, a thin TypeScript-friendly wrapper over IndexedDB, as its primary database. The schema spans 11 tables:

export class TalliofiDatabase extends Dexie {
  plans!: Table<Plan, string>;
  expenses!: Table<ExpenseItem, string>;
  goals!: Table<Goal, string>;
  assets!: Table<Asset, string>;
  liabilities!: Table<Liability, string>;
  snapshots!: Table<MonthlySnapshot, string>;
  netWorthSnapshots!: Table<NetWorthSnapshot, string>;
  changelog!: Table<ChangeLogEntry, string>;
  recurringTemplates!: Table<RecurringTemplate, string>;
  attachments!: Table<ExpenseAttachment, string>;
  vault!: Table<EncryptedVaultRecord, string>;
}

The schema is versioned. Dexie runs migrations sequentially when the schema version increases:

db.version(1).stores({ plans: 'id, name', expenses: 'id, planId', /* ... */ });
// ...
db.version(9).stores({
  // Adds compound index for date-range queries — critical for reports
  expenses: 'id, planId, [planId+transactionDate]',
});

That [planId+transactionDate] compound index in version 9 is a good example of performance-driven schema evolution. Expense reports filter by plan and date range constantly. Without the compound index, every date-range query scans all expenses for a plan. With it, IndexedDB finds the exact range in a single lookup. Schema versions let you add this kind of optimization incrementally, without a big-bang migration.

The result: opening Talliofi after a week away feels instant. No API call, no loading spinner, no authentication handshake. IndexedDB serves data in milliseconds.

Layer 2: TanStack Query as a Smart Cache, Not the Source of Truth

Here is the architectural distinction that most React apps get wrong: TanStack Query is a cache, not a database.

In a typical React + React Query app:

Component -> useQuery -> fetch from API -> display

In Talliofi:

Component -> useQuery -> read from Dexie -> display

The query function talks to IndexedDB, not a network endpoint. TanStack Query still earns its keep: caching, deduplication, background refresh, and stale-time management are genuinely useful even when the data source is local:

export function useExpensesByDateRange(
  planId: string | undefined,
  startDate: string,
  endDate: string,
) {
  return useQuery({
    queryKey: queryKeys.expensesRange(planId!, startDate, endDate),
    queryFn: () =>
      getExpenseRepo().getByPlanIdAndDateRange(planId!, startDate, endDate),
    enabled: !!planId && !!startDate && !!endDate,
    staleTime: PLAN_DATA_STALE_TIME_MS,
  });
}

The staleTime constant means Talliofi does not re-read from IndexedDB on every render. Fresh data serves from the in-memory cache. When a mutation writes to IndexedDB, it invalidates the relevant query keys, triggering a re-read. The component always shows the latest data without polling.

This separation is also what makes the next layer possible.

Layer 3: The Repo Router, One Interface, Three Backends

The central abstraction that makes local-first + optional sync work is the repo router. Every data access in the entire app goes through a function like this:

function isCloudMode(): boolean {
  const mode = useSyncStore.getState().storageMode;
  return mode === 'cloud' || mode === 'encrypted';
}

export function getExpenseRepo(): typeof expenseRepo {
  return isCloudMode() ? supabaseExpenseRepo : expenseRepo;
}

Components and hooks never import a specific repo directly. They call getExpenseRepo(). The function returns the right implementation based on the user’s chosen storage mode. Three modes exist:

Mode Backend When to use
local Dexie only Privacy-first, no cloud account needed
cloud Supabase (unencrypted) Multi-device sync, standard cloud trust
encrypted Supabase (AES-256-GCM) Multi-device sync, zero-knowledge privacy

The Dexie repo and the Supabase repo implement the same interface. Adding a new entity type means writing a new Dexie repo, a new Supabase repo, and registering both in the router. The components never change.

The Data Flow

User action (add expense)
        |
        v
useCreateExpense() hook
        |
        v
getExpenseRepo() --> local?  --> expenseRepo (Dexie)
                 -- cloud?  --> supabaseExpenseRepo (Supabase)
        |
        v
TanStack Query cache invalidated
        |
        v
UI re-renders with fresh data
        |
        v (async, fire-and-forget)
changelog table --> sync engine --> Supabase (on next push)

The sync engine runs in the background. A successful write to Dexie also writes to the local changelog. The sync engine periodically pushes changelog entries to Supabase and pulls remote changes. The UI never waits for sync to complete, local writes are instant.

Layer 4: The Sync Engine, Changelog Over Full-Table Sync

Syncing all data on every cycle works for small datasets but collapses as data grows. Talliofi uses a changelog-based approach instead.

Every create, update, and delete operation appends an entry to a local changelog table:

interface ChangeLogEntry {
  id: string;
  planId: string;
  entityType: 'expense' | 'bucket' | 'plan' | 'goal' | 'asset' | 'liability';
  entityId: string;
  operation: 'create' | 'update' | 'delete';
  timestamp: string;
  payload?: string; // Serialized entity data (encrypted if mode is 'encrypted')
}

On sync, the engine pushes only entries newer than the last sync watermark, a timestamp stored per plan. On pull, it fetches remote entries newer than the watermark and applies them locally.

Conflict resolution uses last-writer-wins with a special case for deletes:

if (row.operation === 'delete' && localEntry) {
  await table.delete(row.entity_id); // Delete always wins
} else if (localEntry && localEntry.timestamp >= row.timestamp) {
  continue; // Local is newer, skip remote
} else {
  await table.put(entity); // Apply remote entry
}

This is a deliberate trade-off worth naming. Last-writer-wins is simple and predictable, but concurrent edits from two devices will produce a winner and a loser. For a personal finance app used primarily on one device at a time, this is exactly the right call. CRDTs or operational transforms would add enormous complexity for a conflict profile that almost never occurs in practice.

Layer 5: End-to-End Encryption with Web Crypto API

The encrypted storage mode goes further: payloads are encrypted client-side before they ever reach Supabase. The server stores opaque ciphertext. Even a full database breach yields nothing useful.

The implementation uses only the browser’s native Web Crypto API, no external cryptography libraries:

export async function deriveKey(
  password: string,
  salt: Uint8Array,
): Promise<CryptoKey> {
  const encoder = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    encoder.encode(password),
    'PBKDF2',
    false,
    ['deriveKey'],
  );
  return crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt,
      iterations: 600_000, // OWASP recommended minimum for SHA-256
      hash: 'SHA-256',
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt'],
  );
}

The encrypted payload structure:

export interface EncryptedPayload {
  iv: string;         // Base64-encoded random IV (unique per encryption)
  ciphertext: string; // Base64-encoded AES-GCM ciphertext (includes auth tag)
  salt: string;       // Base64-encoded salt for key derivation
}

Four design decisions anchor this layer:

600,000 PBKDF2 iterations. OWASP’s current recommendation for PBKDF2-SHA-256. This makes brute-force attacks on the password computationally expensive. Key derivation takes a noticeable moment on first login. We accept that cost.

AES-GCM, not AES-CBC. GCM provides authenticated encryption, decryption verifies the ciphertext has not been tampered with. If an attacker modifies stored data, decryption fails loudly rather than silently producing garbage. This matters when untrusted network paths are involved.

No external crypto libraries. The Web Crypto API ships in every modern browser, is implemented in C++ rather than JavaScript, and carries zero supply-chain risk. Depending on crypto-js for security-critical code adds unnecessary attack surface.

“Bring Your Own Supabase.” The user provides their own Supabase project URL and anon key. Talliofi never touches a server we control. The user owns the entire backend. This is the strongest possible privacy guarantee short of no sync at all.

The UX Result

The architecture produces a specific, demonstrable user experience:

  • First load: Fully functional within 200ms. No API call required.
  • Offline use: Identical to online use. Adding, editing, deleting data, rendering reports — all local.
  • Return after absence: Open the app after a week and see your data instantly, then sync in the background if enabled.
  • No account, no friction: New users start planning immediately. Onboarding never asks for an email address.

This experience is only possible because IndexedDB is the source of truth and the network is a sync layer, not a dependency.

What This Architecture Costs

No free lunch. Local-first has real trade-offs, and pretending otherwise would be dishonest.

Storage is bounded. IndexedDB limits vary by browser and device. For a personal finance app with text-based records, this is not a practical concern, years of expenses fit in a few megabytes. Receipt images are a different story. Talliofi stores attachments as base64, which works but is inefficient. A blob-storage backend would be cleaner.

Sync conflicts are possible. Two devices editing the same expense simultaneously will resolve by timestamp. Most users will never notice. Power users who genuinely edit from two devices at once will occasionally see a stale value overwritten. The app surfaces sync status but does not expose conflict details to the user.

Testing is more complex. Code that writes to IndexedDB requires a DOM-capable test environment. Vitest with jsdom handles this, but setup is more involved than mocking a fetch call. The repo abstraction helps, unit tests inject a mock repo, integration tests hit a real in-memory IndexedDB instance.

Migration is manual. Schema migrations in Dexie are code, not SQL. Version 9 has 9 sequential migration steps. Manageable today, but it demands discipline as the schema evolves.

Why This Pattern Scales

The three-mode architecture earns its complexity when you see what it enables for free:

  • Full offline support: Already there. No extra code needed.
  • Export/import: Read from Dexie, serialize to JSON/CSV. Works offline.
  • Multi-plan scenarios: Each plan is an isolated namespace in IndexedDB. Switching plans is instant.
  • Multi-currency: Exchange rates cached locally. Currency conversion works offline with last-known rates.
  • Encryption: Wraps the existing Supabase layer. The Dexie layer stays untouched.

Every feature that reads or writes data gets offline support automatically because IndexedDB is always the first stop. The sync engine handles getting changes to the cloud eventually. Features ship faster because the hard problem, “where does the data live?”, is already solved.

Start Here

If you are building a web app where data privacy matters, the local-first model is worth the investment. The core pattern is five layers deep, but each layer is straightforward on its own:

  1. IndexedDB (via Dexie) as the source of truth — not the server.
  2. TanStack Query as a cache — with query functions that read from IndexedDB, not fetch from APIs.
  3. A repo interface — implemented by both a local Dexie repo and a remote sync repo.
  4. A routing function — that selects the right repo based on user preference.
  5. A sync engine — that runs in the background, never in the critical path.

The user gets privacy by default and cloud sync as a deliberate choice. That is a better deal than most apps offer. And it is achievable with standard web platform APIs — no proprietary infrastructure, no vendor lock-in, no trust required.

Your data, on your device, on your terms. That should not be a radical idea. But until more apps are built this way, it still is.

Visit Talliofi Website

You’re Not Normal. That’s the Point.

It is Friday at 5pm and you are pretending to care about that last Slack message.

But your brain is already somewhere else.

You have been thinking about that side project all week. The one you scribble architecture for during standups. The one you “accidentally” have three tabs open for right now.

Here is something most people will never understand about you: you go home after a full day of building things, and then you build more things. For fun. On purpose.

That is not normal behavior. That is superhuman behavior.

You live in a world most people do not get. Some of your friends think “API” is a type of beer. Your family describes your job as “something with computers.” And yet here you are, halfway between machine learning papers and Stack Overflow at midnight, quietly building the future while everyone else is watching Netflix.

And if you are reading this right now thinking “I never finish anything” — relax. Most shipped products started as abandoned repos that someone came back to on a random Saturday because they could not sleep.

You are not behind. You are just loading.

The difference between a side project and a startup is about 3 weekends and one mass DM to your friends that says “hey can you test something for me, it will only take 2 minutes.”

It will not take 2 minutes.

Here is what I have learned watching hundreds of builders at hackathons, in Discord servers, and across dev communities: the ones who ship are not the most talented. They are the ones who start before they are ready and ask for help before they are stuck.

That is it. That is the whole playbook.

But let me say this clearly: the world needs you. Not in a corny motivational poster way. In a real way.

We are living through the biggest technology shift most people will ever see, and the majority of the population does not understand what is happening. You do. That makes you rare. Do not take that for granted, and do not shame anyone who has not caught up yet.

Bring them along. That is what superhumans do.

A lot of the weekend builders I talk to are deep into AI right now. Building agents, adding memory to chatbots, wiring up RAG pipelines, trying to make their apps actually remember context instead of starting from scratch every conversation.

That is exactly why we built Backboard. It is the infrastructure layer for AI apps — memory, state management, model routing, RAG, tool calls — all through one API. No stitching five tools together. No losing your conversation history every time you swap models.

Here is the weekend deal: sign up and you get free state management for life plus free dev credits to play with. No catch. No trial that expires on a Tuesday when you forgot to cancel. Just go build something, break something, and let us know what you think.

We built this for weekend builders. Your feedback is literally how we get better.

So this weekend, if you ship something, tag me. If you break something, tag me faster. And if you spend the whole weekend staring at your IDE with a blank file open, just know — that is also part of the process.

Have fun this weekend, super devs. See you on the other side of Sunday.

My GSoC 2026 Journey: Spectra.jl across the Electromagnetic Spectrum

I’m applying for Google Summer of Code 2026 with JuliaAstro
to work on Spectra.jl — a Julia package for loading and
manipulating astrophysical spectra across the electromagnetic spectrum.

My goal is to migrate the OGIP parser from SpectralFitting.jl
into Spectra.jl, implement loaders for optical, infrared, and
radio spectral formats, and add common manipulation routines
like rebinning and unit conversion.

I’ve been exploring the codebase, reading the OGIP standard
documentation, and engaging with the mentors on GitHub issues #41 and #47.

Follow my journey here as I document my progress!

How a Branded Cents Type Eliminated an Entire Class of Bugs Across 97 Files

TypeScript branded types turned compile-time into our most reliable financial auditor here is the pattern, the code, and the lessons from deploying it across a real production PWA.

Estimated reading time: 9 minutes

The Penny That Should Not Exist

Open your browser console right now and type 0.1 + 0.2. Go ahead, I will wait.

You got 0.30000000000000004. Not 0.3. Every developer discovers this at some point and shrugs it off. But when you are building software that handles someone’s rent payment, their emergency fund, their debt payoff plan that invisible four-quadrillionth of a cent compounds. It hides in running totals. It lurks in tax calculations. It turns a perfectly balanced budget into one that is off by a penny, and the user stares at the screen wondering if they can trust anything your app is telling them.

I hit this wall while building Talliofi, a local-first financial planning PWA built with React 19 and TypeScript. The app handles income, expenses, budgets, goals, net worth tracking, and multi-currency conversion. Money touches every feature. And every feature was a potential floating-point landmine.

The fix was not a library. It was not a dependency. It was twelve lines of TypeScript that now protect 928 occurrences of monetary values across 97 files in the codebase.

Why Floating-Point Arithmetic Hates Your Money

This is not a new problem. IEEE 754 double-precision floating-point numbers cannot represent most decimal fractions exactly. The number 0.1 in binary is a repeating fraction, like trying to write one-third in decimal. You get close, never exact.

For most applications, “close” is fine. For money, “close” is a lawsuit.

The classic fix is to work in the smallest unit of currency. Instead of storing $19.99 as 19.99, you store 1999, an integer representing cents. Integer arithmetic in JavaScript is exact up to Number.MAX_SAFE_INTEGER (about 9 quadrillion cents, or $90 trillion). That is plenty of headroom for a personal finance app.

But here is the thing nobody tells you: converting to integer cents solves the math problem, not the human problem.

A developer on your team writes a function that accepts a number. Is that number dollars or cents? Is it pre-tax or post-tax income? The type system says number, which tells you nothing. Someone passes in 19.99 where 1999 was expected, and you have a bug that is off by a factor of 100 — far worse than a floating-point rounding error.

I needed the compiler itself to refuse bad money.

The Naive Fix (And Why It Still Fails)

The first attempt is always the same. You create a convention: “All monetary values end in Cents.” You name your variables amountCents, incomeCents, balanceCents. You write it in the README. You mention it in code review.

Then someone writes:

const taxRate = 0.22;
const grossIncome: number = 500000; // cents
const tax: number = grossIncome * taxRate; // 110000 -- correct, but is it Cents?
const netIncome: number = grossIncome - tax; // works fine... this time

Everything compiles. Everything runs. The bug arrives three weeks later when a different developer passes netIncome into a function that expects dollars instead of cents. Or when tax gets rounded to a float and passed into another calculation that assumes integer cents.

Naming conventions are documentation. Documentation lies. Types do not.

Enter TypeScript Branded Types

A branded type is a TypeScript pattern that creates a distinct type from a primitive without any runtime overhead. The entire implementation in Talliofi looks like this:

// The branded type: a number that has been validated as integer cents
export type Cents = number & { readonly __brand: 'Cents' };

// The only way to create a Cents value -- our validation gate
export function cents(value: number): Cents {
  if (!Number.isSafeInteger(value)) {
    throw new Error(`Invalid cents value: ${value}`);
  }
  return value as Cents;
}

That & { readonly __brand: 'Cents' } intersection is the key. At runtime, it does not exist. There is no extra property on the number. JavaScript has no idea it is there. But TypeScript treats Cents as a distinct type from number. You cannot pass a raw number where Cents is expected without an explicit cast or going through the cents() constructor.

The constructor is your validation gate. Every monetary value in the system must pass through it. If you try to create Cents from 19.99 (a float), NaN, Infinity, or a number beyond safe integer range it throws at runtime. Two layers of protection: compile-time type checking and runtime validation.

For non-technical readers, think of it like a currency exchange booth at an airport. You cannot just hand someone random paper and call it money. You go through the booth, they validate it, stamp it, and now it is recognized currency. The cents() function is that booth.

Why Even Addition Gets Its Own Function

Once you have a branded type, you cannot just add two Cents values with +. The result of a + b in TypeScript is number, not Cents. Your carefully validated types would leak back to raw numbers after a single operation.

So every arithmetic operation gets a wrapper:

export const addMoney = (a: Cents, b: Cents): Cents => cents(a + b);
export const subtractMoney = (a: Cents, b: Cents): Cents => cents(a - b);

// Multiplication and percentage introduce fractions -- Math.round keeps us in integers
export const multiplyMoney = (amount: Cents, factor: number): Cents =>
  cents(Math.round(amount * factor));
export const percentOf = (amount: Cents, percent: number): Cents =>
  cents(Math.round(amount * (percent / 100)));

// Summing an array -- starts from validated zero, accumulates through addMoney
export const sumMoney = (amounts: readonly Cents[]): Cents =>
  amounts.reduce((sum, amt) => addMoney(sum, amt), cents(0));

Notice something subtle: multiplyMoney and percentOf accept a plain number as the second argument, not Cents. That is intentional. A tax rate is not money. A multiplication factor is not money. The type signatures enforce this distinction. You cannot accidentally pass a Cents value where a percentage belongs.

Here is the gotcha that surprises most developers: addMoney re-validates through cents() on every operation. That means overflow protection is automatic. If two enormous Cents values added together exceed Number.MAX_SAFE_INTEGER, the cents() constructor throws immediately rather than silently producing a corrupt value. Our test suite verifies this explicitly:

it('addMoney throws when result exceeds MAX_SAFE_INTEGER', () => {
  const large = cents(Number.MAX_SAFE_INTEGER - 1);
  expect(() => addMoney(large, cents(2))).toThrow('Invalid cents value');
});

Division by zero? Also caught. Math.round(100 / 0) produces Infinity, and cents(Infinity) throws. No special handling needed — the validation gate catches everything.

The Performance Play: Caching Intl.NumberFormat

Formatting money for display happens constantly in a financial app. Every list item, every chart tooltip, every summary card. Creating a new Intl.NumberFormat object on every render is expensive — these constructors parse locale data and build internal state.

Talliofi caches formatters by locale and currency:

const formatterCache = new Map<string, Intl.NumberFormat>();

export function formatMoney(
  amount: Cents,
  options: { locale?: string; currency: CurrencyCode },
): string {
  const { locale = 'en-US', currency } = options;
  const key = `${locale}:${currency}`;   // e.g., "en-US:USD" or "de-DE:EUR"
  let formatter = formatterCache.get(key);
  if (!formatter) {
    formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
    formatterCache.set(key, formatter);
  }
  return formatter.format(centsToDollars(amount));
}

The function signature enforces correctness: it accepts Cents, not number. You cannot accidentally format raw dollars or an unconverted value. The conversion to display-friendly dollars happens once, at the very last moment, inside this function. Everywhere else in the system, money stays in its branded integer form.

With six supported currencies and their associated locales, the cache holds at most a handful of entries. The memory cost is negligible. The performance gain on rendering a list of 200 expenses is noticeable.

Being Honest About Uncertainty: The ConversionResult Pattern

Multi-currency support introduced a design question I had not anticipated. When you convert USD to EUR and the exchange rate is unavailable (the user is offline, the rate API is down, the currency pair is not configured), what do you return?

Returning the original amount silently is dangerous the user would see “$500” labeled as “EUR 500” and never know the conversion failed. Throwing an error is disruptive for a display-only calculation. Returning null forces every consumer to handle the missing case.

Talliofi uses a tagged result:

export interface ConversionResult {
  readonly amount: Cents;
  /** Whether the amount was actually converted using a real exchange rate */
  readonly converted: boolean;
}

Callers get their Cents value regardless, but they also get a boolean that says “I actually converted this” versus “I just gave you back the original because I did not have a rate.” The UI can then decide how to present this showing a warning icon, displaying “approximate” text, or fetching fresh rates.

This pattern is worth stealing for any domain where a function might produce a plausible-looking result from incomplete data. Do not hide uncertainty. Type it.

What 928 Occurrences Across 97 Files Looks Like

The Cents type is not a novelty confined to a utility file. It flows through the entire domain:

// Plans track income in Cents
export interface Plan {
  readonly grossIncomeCents: Cents;
  // ...
}

// Every expense uses Cents -- no raw numbers allowed
export interface ExpenseItem {
  readonly amountCents: Cents;
  // ...
}

// Split transactions carry Cents per split
export interface ExpenseSplit {
  readonly amountCents: Cents;
  // ...
}

// Goals, assets, liabilities, snapshots -- all branded
export interface Goal {
  readonly targetAmountCents: Cents;
  readonly currentAmountCents: Cents;
}

From income calculations to tax estimates to budget summaries to multi-month trend charts, every monetary value in Talliofi is Cents. The branded type creates a closed system. Money enters through cents() or dollarsToCents(), stays branded through all calculations, and only converts back to a display-friendly number at the very edge — inside formatMoney(). A new developer joining the project cannot accidentally create monetary bugs without the compiler flagging it.

What Almost Broke Me

The hardest part was not the type system. It was the migration.

When I introduced the branded type, every function that touched money needed to change. Not just the types, the call sites. Every place that did income - expenses had to become subtractMoney(income, expenses). Every array.reduce((sum, x) => sum + x.amount, 0) had to become sumMoney(items.map(i => i.amountCents)).

The compiler caught every one. That was the point. But “the compiler tells you where to fix things” is different from “the compiler fixes things for you.” It was a methodical, file-by-file grind.

The lesson: introduce branded types early. The cost of adopting them grows linearly with your codebase. In a fresh project, it is nearly free. In a project with 97 files touching money, it is a weekend.

What I Learned (If I Built This Again)

1. Brand early, brand aggressively. I would introduce Cents in the very first commit, before any business logic exists. The migration cost is real and grows with time.

2. The constructor is the entire security model. The cents() function is one of the most important functions in the codebase. It is small, it is simple, and it is the single chokepoint for monetary validation. Guard it. Test it. Never bypass it.

3. This pattern generalizes far beyond money. Percentages (0–100 vs 0–1), timestamps (milliseconds vs seconds), distances (meters vs feet), IDs (user IDs vs order IDs), any domain where raw numbers carry implicit meaning is a candidate for branding.

4. Readonly interfaces compound the safety. Every domain type in Talliofi uses readonly properties. Combined with branded types, this means monetary values cannot be created incorrectly (branded constructor) or mutated incorrectly (readonly fields). Two fences, one enclosure.

5. The converted: boolean pattern pays for itself. Anywhere your code produces a “best effort” result, tag it with a confidence signal. Do not make downstream consumers guess whether the value is real or approximate.

Start Here

If you take one thing from this article, take the twelve lines. Copy the Cents type and the cents() constructor into your next TypeScript project that touches money. Write the arithmetic wrappers. Make your compiler do the auditing.

The pattern is small. The protection is not.

The best type systems do not just describe your code. They make entire categories of mistakes impossible to write.

Visit Talliofi website

The Webhook Failure Modes Nobody Warns You About

The Webhook Failure Modes Nobody Warns You About

Until you’re staring at a 78-hour Stripe retry schedule wondering why your handler never fired.

Webhook integrations look simple. You point Stripe at your endpoint, you get a 200 OK, you’re done.

Then something breaks in production — and you have no idea what. Was it the payload? The signature? Your server? Stripe’s retry queue? Something downstream?

This isn’t a guide to building webhook endpoints. It’s a field guide to the failures developers actually hit — and how to debug them faster.

Failure Mode 1: The Silent Drop

Your endpoint returns 200 OK to Stripe. But your handler never fires.

What actually happened: your server accepted the request, but the payload was routed to a different part of your application that silently failed. Or the event was filtered by a middleware. Or the webhook was received but the database write failed and the error was swallowed.

The classic debug move: pepper your handler with console.log(). Deploy. Wait for Stripe to retry (up to 78 hours). Check logs. Repeat.

Faster way: capture the raw webhook before it reaches your server. See exactly what was sent, what your endpoint returned, and when — without touching your application code.

Failure Mode 2: The Empty Payload

Stripe says it sent the event. Your handler receives it. But event.data.object is null or {}.

This is almost always an API version mismatch — your endpoint is using a different Stripe API version than the one that generated the event. Stripe doesn’t warn you; it just sends what it has.

The catch: you can’t see the raw payload your endpoint received. You only see what your code parsed. And if the parsing failed silently, the debug path starts from the wrong place.

Fix: log the raw request body at the very top of your handler, before any parsing, before any middleware.

Failure Mode 3: The Signature Validation Loop

You’re validating the Stripe signature. It keeps failing. You regenerate your webhook secret. It still fails. You hardcode the raw payload to test — it works.

The issue is almost always that you’re validating the parsed body instead of the raw body. Stripe’s signature is computed over the raw request body, not the JSON object your framework deserialized.

// Wrong — app.use(bodyParser.json()) already parsed this
app.post('/webhook', (req, res) => {
  const sig = req.headers['stripe-signature'];
  stripe.webhooks.constructEvent(req.body, sig, secret); // req.body is PARSED
});

// Right — use raw body
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  stripe.webhooks.constructEvent(req.body, sig, secret); // req.body is RAW
});

If you’ve ever fought a Stripe signature validation error for an hour, you know this pain.

Failure Mode 4: The Infrastructure Block

Your endpoint is behind an ngrok tunnel that expired. Your firewall is blocking incoming requests from Stripe’s IP range. Your server restarted and the process isn’t back up yet.

Stripe reports delivery failures. Your server never received the event. Nobody knows why.

The problem: you have no visibility into whether your endpoint is actually reachable from the public internet. You find out something is wrong when Stripe emails you about failed deliveries.

Failure Mode 5: The Retry Wait

Your handler has a bug. You fix it. You need to test it.

Stripe’s retry schedule: 1 hour, 12 hours, 72 hours. That’s potentially 78 hours of waiting to verify your fix works.

For payment webhooks, Stripe will eventually retry — but waiting that long in development is brutal. The alternative is triggering test events manually through the Stripe CLI, which works but doesn’t capture the full production scenario.

The Better Debug Workflow

The common thread in all these failures: you can’t see what’s actually happening.

A webhook debugging tool gives you a dedicated endpoint that sits between the sender (Stripe, GitHub, etc.) and your server:

  1. Capture — the sender hits your debug endpoint instead of your server directly
  2. Inspect — you see the full raw payload, headers, and timing in a dashboard
  3. Replay — you resend the exact payload to your server instantly, without waiting for retries

This means you catch failures in seconds, not hours. You see what your server actually received — not what you hope it received.

Hooklog — Webhook Debugging Without the Wait

I built Hooklog to solve exactly this. It’s free (10k events/month), requires no signup, and gives you:

  • A unique webhook URL you can point any service at immediately
  • Full payload inspection: headers, body, response, timing
  • One-click replay to test your handler without waiting for retries
  • Email alerts when your endpoint returns 4xx or 5xx

Get started: https://hooklog.c0c58bd.p.egbe.app

Free tier: 10,000 events/month, 3-day retention, up to 3 endpoints. No credit card required.

What’s your worst webhook debugging story? Drop it in the comments — I read every one.

Security news weekly round-up – 27th March 2026

While cybersecurity defenders are looking for innovative ways to keep Internet users safe, cybercriminals are doing the opposite — to hurt users by stealing their money or information that can lead to theft or other things that are valuable to the user. It’s upon me and you to always know the threat out there and act accordingly.

Hackers Use Fake Resumes to Steal Enterprise Credentials and Deploy Crypto Miner

It’s a phishing attack. To complicate issues, if you fall for it, it takes around 25 seconds from script execution to credential exfiltration.

Here is what’s going on:

The initial dropper file is a Visual Basic Script (VBScript) that, upon opening, displays a bogus French-language error message, fooling message recipients into thinking that the file is corrupted.

…the heavily obfuscated script runs a series of checks to evade sandboxes and enters into a persistent User Account Control (UAC) loop that prompts users to run it with administrator privileges.

As soon as the dropper obtains administrative privileges, it wastes no time disabling security controls and covering up its tracks

Convicted spyware chief hints that Greece’s government was behind dozens of phone hacks

While reading the article, one thing is clear: someone hacked the phones of government officials and journalists. The spyware chief was sentenced to eight years in prison and now he claims he will not be a “scapegoat.”

From the article:

Several senior officials in the Greek government, including the head of Greece’s national intelligence agency and a senior aide to the Prime Minister Kyriakos Mitsotakis, resigned in the wake of revelations that several journalists’ phones had been hacked.

No government officials have been convicted in connection with the surveillance, and critics have accused the Mitsotakis government of a cover-up.

WebRTC Skimmer Bypasses CSP to Steal Payment Data from E-Commerce Sites

At the time of writing, Adobe has patched the vulnerability that allowed this to happen, but it appears that the patch is yet to reach production websites.

The following is how the skimmer works:

The skimmer is designed as a self-executing script that establishes a WebRTC peer connection to a hard-coded IP address (“202.181.177[.]177”) over UDP port 3479 and retrieves JavaScript code that’s subsequently injected into the web page for stealing payment information.

The use of WebRTC marks a significant evolution in skimmer attacks, as it bypasses Content Security Policy (CSP) directives.

Apple made strides with iOS 26 security, but leaked hacking tools still leave millions exposed to spyware attacks

The article title says it all. If you’re not in the loop in the past few weeks, let me update you. The hacking tools in question are DarkSword and Coruna. The former was leaked on GitHub, making it easy for anyone to launch attacks on older iOS users.

From the article:

The discovery of Coruna and DarkSword suggest that memory-based attacks could continue to plague users of older iPhones and iPads that lag behind the newer, more memory-safe models.

Experts working for iVerify and Lookout, two cybersecurity companies that have a commercial stake in selling security products for mobile devices, say Coruna and DarkSword may also challenge the long-held assumption that iPhone hacks are rare.

Credits

Cover photo by Debby Hudson on Unsplash.

That’s it for this week, and I’ll see you next time.