🚀 Stop Building OTP Systems from Scratch — I Built a Complete Redis-Based Verification Engine for Node.js

🚀 Stop Building OTP Systems from Scratch — I Built a Complete Redis-Based Verification Engine for Node.js

Authentication flows look simple… until you actually build them in production.

At first, I thought OTP verification was just:

  • Generate a code
  • Store it
  • Verify it

But very quickly, things started breaking down.

🤯 The Hidden Complexity of OTP Systems

In real-world applications, OTP is just the beginning.

You also need:

  • ⏳ Expiry handling
  • 🔁 Retry limits
  • 🚫 Abuse prevention (brute force)
  • 🔑 Token-based verification
  • 🔗 Email verification links (magic links)
  • ⚡ High performance under load

👉 A “simple OTP system” quickly becomes a full verification infrastructure.

😤 The Problem with Existing Solutions

While exploring existing libraries, I noticed:

  • ❌ Too many dependencies
  • ❌ Over-engineered abstractions
  • ❌ Tight coupling with email/SMS providers
  • ❌ Not flexible for custom flows

Most libraries solve one problem, but not the whole system.

💡 The Idea: A Unified Verification Layer

Instead of stitching together multiple tools, I built:

👉 redis-otp-manager

A lightweight, Redis-powered verification engine that handles:

  • 🔐 OTPs
  • 🔑 Tokens
  • 🔗 Magic links

All in one place.

⚙️ Why Redis is Perfect for This

Redis solves most of the hard problems out of the box:

  • ⏳ Built-in TTL → automatic expiry
  • ⚡ In-memory performance → blazing fast
  • 🔒 Atomic operations → avoids race conditions

👉 No cron jobs. No cleanup scripts. No complex DB queries.

✨ What This Package Actually Does

This is not just an OTP library — it’s a complete verification system.

🔐 OTP (One-Time Password)

  • Generate secure OTPs
  • Validate with expiry
  • Limit retries

🔑 Token-Based Verification

  • Generate secure tokens
  • Ideal for backend validation
  • More secure than short OTPs

🔗 Magic Links (Verification URLs)

  • One-click verification
  • Perfect for:
    • Email verification
    • Password reset
    • Passwordless login

Example:

https://yourapp.com/verify?token=abc123

📦 Installation

npm install redis-otp-manager

🚀 Getting Started

1️⃣ Setup Redis Client

const { createClient } = require(“redis”);

const client = createClient();
await client.connect();

2️⃣ Initialize Manager

const { RedisOtpManager } = require(“redis-otp-manager”);

const manager = new RedisOtpManager(client);

🔐 OTP Example

const otp = await manager.generate(“[email protected]“);

// send OTP via email/SMS

const isValid = await manager.verify(“[email protected]“, otp);

🔑 Token Example

const token = await manager.generateToken(“[email protected]“);

const isValid = await manager.verifyToken(“[email protected]“, token);

🔗 Magic Link Example

const { token, url } = await manager.generateLink(“[email protected]“);

// send URL via email

await manager.verifyToken(“[email protected]“, token);

🧠 How It Works (Internals)

  • Each request is stored in Redis with a unique key
  • TTL ensures automatic expiration
  • Verification checks:
    • Value match
    • Expiry
    • Retry limits

👉 One-time usage ensures security.

⚡ Key Advantages

🚀 Performance

  • No database queries
  • Fully in-memory

🧩 Simplicity

  • Minimal API
  • Easy integration

🔒 Security

  • Expiry enforced
  • Retry limits
  • One-time usage

🧱 Flexibility

  • Works with any backend
  • No dependency on email/SMS providers

🆚 Traditional vs Modern Approach

Feature| Traditional OTP| redis-otp-manager
Storage| Database| Redis
Expiry| Manual| Automatic
Scaling| Hard| Easy
Links support| ❌| ✅
Tokens| ❌| ✅

⚡ Real-World Use Cases

  • User signup verification
  • Password reset
  • Email verification
  • Passwordless login
  • Microservices authentication

🙌 Open Source

If you find this useful:

  • ⭐ Star the repo
  • 🐛 Report issues
  • 🤝 Contribute

🔗 NPM Package

👉 https://www.npmjs.com/package/redis-otp-manager

💬 Final Thoughts

OTP is not just a feature — it’s part of a larger verification system.

By combining OTPs, tokens, and magic links into a single unified layer, you can:

  • Reduce complexity
  • Improve performance
  • Build scalable authentication systems

If you’re building auth systems in Node.js…

👉 This might save you hours of engineering time.

Qdrant Has a Free API You Should Know About

Qdrant is an open-source vector database designed for AI applications. It stores and searches high-dimensional vectors with blazing speed — perfect for semantic search, recommendations, and RAG.

Why Qdrant Powers Modern AI Apps

A startup building a RAG chatbot was storing embeddings in PostgreSQL with pgvector. At 10M vectors, queries took 3 seconds. They switched to Qdrant — same queries now take 15ms.

Key Features:

  • Fast Search — HNSW algorithm for sub-millisecond queries
  • Filtering — Combine vector similarity with metadata filters
  • Payload Storage — Store arbitrary data alongside vectors
  • Quantization — Reduce memory usage by 4-32x
  • Distributed — Horizontal scaling with sharding

Quick Start

docker run -p 6333:6333 qdrant/qdrant

Python SDK

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance

client = QdrantClient("localhost", port=6333)

client.create_collection(
    collection_name="articles",
    vectors_config=VectorParams(size=384, distance=Distance.COSINE)
)

client.upsert(
    collection_name="articles",
    points=[{"id": 1, "vector": embedding, "payload": {"title": "My Article"}}]
)

results = client.search(
    collection_name="articles",
    query_vector=query_embedding,
    limit=5
)

Why Choose Qdrant

  1. Blazing fast — optimized for production AI workloads
  2. Rich filtering — combine vector search with metadata queries
  3. Open source — self-host or use Qdrant Cloud

Check out Qdrant docs to get started.

Building AI applications? Check out my Apify actors or email [email protected] for data extraction solutions.

I built an all-in-one PDF Scanner & Vault app solo in Kotlin — Lihenium is now live on Play Store!

Lihenium App Logo

Hey DEV community!
I’m Abhishek, a solo Android developer from Bihar, India. After months of late-night coding, I launched my app Lihenium: Scan PDF & Vault on Google Play Store!

Lihenium App Logo

🤔 Why I Built It

I was frustrated using 5 different apps just to scan, edit, manage, compress and lock my documents. So I built one app that does everything — offline-first, private by default.

🛠️ Tech Stack

  • Kotlin + Jetpack Compose
  • OpenCV for smart document corner detection
  • Room Database for local storage
  • CameraX for scanning
  • ML Kit for OCR

📱 Core Features

App Main Dashboard

  • ✂️ PDF Tools: Merge, split, sign, OCR & watermark
  • 📷 QR Scanner & Generator
  • 📄 Smart Document Scanner with auto-crop

Scanner Screen

  • 🔒 Secure Vault with biometric lock

Secure Vault

  • 📁 Doc Library with folders & tags

Doc Library

  • 🖼️ Passport photo maker & background remover

Passport Photo Maker

  • ☁️ Optional Google Drive backup

Google Drive Backup

💪 Biggest Challenges as Solo Dev

  1. Integrating OpenCV efficiently without bloating the APK
  2. Handling 10,000+ images without OOM crashes
  3. Keeping the final APK at just 18MB with all features packed in

🚀 Try It!

👉 Play Store: https://play.google.com/store/apps/details?id=com.abhi.docu

Would love your honest feedback! 🙏

Life Doc AI: Transforming Notion Journals into Cinematic Documentaries

This is a submission for the Notion MCP Challenge

What I Built

Life Doc AI is a Python-based intelligent web application that transforms your Notion workspace—specifically your Life Events and Journal Entries—into a beautifully structured, cinematic documentary of your personal journey.

Instead of looking at raw text or Kanban boards, Life Doc AI uses the Notion Model Context Protocol (MCP) to securely fetch your milestones and reflections, and relies on an AI Agent (powered by OpenAI GPT-4o with robust rule-based fallbacks) to process them. The result is a Premium Streamlit UI that presents:

  • 📜 The Story of You: An AI-crafted, dramatic narrative of your life.
  • ⏳ Chronological Timeline: Analyzing the dots and connecting your journey.
  • 📚 Era Chapters: Automatically grouping your life into meaningful, thematic chapters.
  • 💡 Psychological Insights: Extracting hidden patterns, your most active years, and measuring your resilience.

Video Demo

Show us the code

Here is the complete open-source repository on GitHub!

🎬 Life Doc AI

Transform your Notion journals and life events into an AI-crafted cinematic documentary of your personal journey.

Life Doc AI connects to your Notion workspace via the Model Context Protocol (MCP), reads your life events and journal entries, and generates a beautiful documentary narrative — complete with timeline, chapters, and psychological insights.

✨ Features

  • 🔌 Notion MCP Integration — Communicates with Notion through MCP protocol (JSON-RPC over HTTP)
  • 📖 Life Events Timeline — Chronological visualization of your milestones
  • 📚 Auto-Generated Chapters — Groups your life into meaningful eras
  • 💡 Psychological Insights — Analyzes patterns, most active years, and impact metrics
  • 📜 AI Narrative — Crafts a cinematic story from your data (via OpenAI GPT-4 or built-in generator)
  • 🎨 Premium UI — Dark theme with glassmorphism, gradient accents, and smooth animations

🏗️ Architecture

┌───────────────────┐     MCP Protocol     ┌──────────────────────┐     Notion API    ┌─────────┐
│  Streamlit App    │ ◄──────────────────► │  Python

…

How I Used Notion MCP

The Notion MCP was the absolute core of this project, bridging the gap between my private workspace and my AI generation engine securely.

I built a custom Python MCP Server (mcp/mcp_server.py) that wraps the Notion REST API and exposes tools like notion_query_database over a JSON-RPC HTTP transport layer.
When the user clicks “Generate”, the Streamlit UI (acting as the MCP Client) fires a remote procedure call to the local server. The MCP Server negotiates with Notion, retrieves the Life Events and Journal Entries databases, and returns a clean, structured JSON payload.

The Developer Journey

This project represents the culmination of my recent developer journey. A few weeks ago, I was figuring out static web portfolios. That evolved into mastering containerized FastAPI backends, and eventually wrestling with complex NLP text preprocessing errors.

When the Notion MCP Challenge was announced, I realized I could combine all those skills:

  • The backend architecture knowledge helped me build a robust, thread-safe Python MCP Server.
  • The NLP experience allowed me to craft intelligent prompts and structured LLM parsing.
  • The web development skills helped me design a “Glassmorphism” Dark Theme Premium UI in Streamlit (.streamlit/config.toml).

Life Doc AI doesn’t just read data; it understands it. The Notion MCP unlocked the ability to treat Notion not just as a note-taking app, but as the engine of a person’s life story.

Using the Creem CLI as a Developer Power Tool with Claude Code

If you build with Creem, you know the loop: write some code, open the dashboard, check if the checkout went through, check the webhook logs, compare with your local state, go back to the code. Four tabs, three data formats, five minutes just to confirm a test purchase landed. Repeat for every change.

This guide shows a different approach. You stay in the terminal. The Creem CLI provides the data, Claude Code interprets it, and your app shows the result. Everything in one place.

I built a small integration to demonstrate this. The repo includes a minimal Bun + Hono app that handles checkouts and webhooks, plus a Claude Code skill that teaches the AI how to operate the Creem CLI safely. The entire workflow — verify, debug, operate — happens from the terminal.

The setup

The integration is intentionally small. A Bun server running Hono that creates checkout sessions, receives and verifies webhook events from Creem using HMAC SHA-256, and persists local state to JSON files. The home page shows a color-coded status badge — green for access granted, red for revoked, yellow for in-progress, gray for no activity — so you can see the current state at a glance without reading JSON.

The app tracks a simple state machine: unknown → checkout-created → checkout-completed → granted → revoked. Every transition is driven by a webhook event. If the webhook doesn’t arrive, the state doesn’t change. This becomes important later.

The key thing to understand is that the Creem CLI and the app are two independent views of the same data. The CLI talks directly to the Creem API. The app only learns about changes through webhooks. When they agree, everything is fine. When they don’t, something broke — and the CLI can help you find out what.

Teaching Claude Code the Creem CLI

Claude Code is a capable AI coding assistant, but it doesn’t know how to use the Creem CLI out of the box. It doesn’t know the commands, the flags, or the conventions — like the fact that Creem returns money amounts in minor units, where 500 means 5.00 EUR, not 500.00.

That’s why I built a skill. In Claude Code, a skill is a structured document that teaches the AI how to use a specific tool. It covers the available commands, the correct flags, safety rules, and common workflows.

The Creem CLI skill includes:

  • A quick reference table for every supported command: products, customers, checkouts, subscriptions, transactions, and configuration
  • Rules like “always use --json for machine-readable output” and “never read or expose the config file at ~/.creem/config.json“
  • Money amount conventions: always convert minor units before showing values to the user
  • High-value workflows: how to create and verify a checkout, how to look up customers, how to manage subscription lifecycle
  • Automation patterns for scripting and piping output through jq

The skill lives in the repo at skills/creem-cli/SKILL.md and anyone can install it with one command:

npx skills add santigamo/creem-cli-developer-toolkit

Once installed, Claude Code loads the skill automatically when it’s relevant. You talk in natural language, and it translates your intent into the correct CLI commands with the right flags and safety constraints.

Workflow 1: Verify a purchase

The first thing you want to know when building a payment integration is: does it work? Did the checkout go through? Did the webhook land? Does my app agree with the payment provider?

Normally you’d check the Creem dashboard for the transaction, then check your server logs for the webhook, then maybe hit your debug endpoint to compare. Three different places, three different formats.

With the CLI skill loaded, the conversation looks like this:

You: “Create a test checkout for my product and give me the URL.”

Claude Code runs creem checkouts create --product <id> --success-url http://localhost:3000/success --json, parses the response, and hands you the checkout URL. You open it, complete the sandbox purchase with a test card, and come back.

You: “I just completed the purchase. Can you verify the transaction went through?”

Claude Code runs creem transactions list --json, finds the new transaction, and explains: paid, the amount in human-readable format (converting from minor units), the associated subscription ID. No raw JSON to read.

You: “Does the subscription look healthy?”

Claude Code runs creem subscriptions get <id> --json and confirms: active, renewal date set, everything nominal.

You: “Check my app’s local state too — does it agree with Creem?”

Claude Code hits curl localhost:3000/api/debug/state, compares the local access status with what the Creem API returned, and confirms they match. The app’s status badge shows green: “Access Granted.”

One conversation, zero dashboard tabs. Claude Code explains the results in plain English, so you don’t have to parse any JSON yourself.

Workflow 2: Diagnose a state mismatch

Verifying the happy path is useful, but the real value shows up when something breaks. Specifically: when your payment provider knows something that your app doesn’t.

This happens more often than you’d think. A subscription gets paused or canceled on the Creem side, but the webhook doesn’t land — maybe your endpoint was down, maybe there was a network hiccup, maybe your tunnel dropped. Your app keeps showing “Access Granted” while the subscription is actually paused. Your user has access they shouldn’t have, or worse, they lost access and your app doesn’t know.

Finding this kind of mismatch by hand means opening the Creem dashboard, finding the subscription, checking its status, then comparing with your database or local state, then checking your webhook logs to see if the event was ever delivered.

Here’s how it looks with the CLI:

You: “Pause the subscription from my last test purchase, then compare what Creem says with what my app says. I want to know if subscription changes are propagating correctly.”

Claude Code runs the pause command, then gathers evidence from both sides: creem subscriptions get <id> --json and creem transactions list --json for the Creem state, and curl localhost:3000/api/debug/state for the app state. It compares them and reports the finding.

In my test, the result was clear: Creem shows the subscription as paused, but the app still says “Access Granted.” The status badge on the home page is still green. The webhook never arrived.

Claude Code didn’t just find the mismatch — it explained why. The subscription changed in Creem, Creem tried to send a webhook, but the endpoint wasn’t reachable. The local state is stale.

You: “What are my recovery options?”

Claude Code suggests concrete steps: restore the webhook endpoint, then either replay the event if the platform supports it, or trigger another state change so the next webhook lands and syncs the state. It adapts the suggestions to what it knows about the setup.

The same investigation through dashboards and logs — opening the subscription page, cross-referencing with webhook delivery logs, checking your server output — would involve a lot more clicking and context-switching. Here it was one prompt and a clear answer.

Workflow 3: Operate the store

The CLI isn’t just for testing and debugging. Once you’re comfortable with it, you can manage your Creem store without leaving the terminal.

First, recovery from the previous workflow. After restoring the webhook endpoint:

You: “My endpoint is healthy again. Resume that subscription and confirm the current state.”

Claude Code resumes the subscription, verifies the state on both sides, and confirms everything is back in sync. The app’s status badge flips back to green, and the webhook event shows up in the recent events table.

Then, product management:

You: “I want to add a Pro Plan to my store. 19 dollars a month, recurring.”

Claude Code runs creem products create with the right flags: --name "Pro Plan", --price 1900 (minor units), --currency USD, --billing-type recurring, --billing-period every-month. One sentence from you, one command from the CLI.

You: “List all my products to confirm.”

Claude Code runs creem products list --json and shows you both products — the original test product and the new Pro Plan.

You: “Give me a summary of the current state of my store.”

Claude Code pulls products and subscriptions, cross-references them, and gives you a plain English overview: how many products, how many active subscriptions, current revenue state. The kind of snapshot that would normally require opening the dashboard and clicking through several pages.

Why this matters

The pattern here isn’t specific to Creem. It’s what happens when a CLI tool produces structured JSON output and an AI assistant knows how to read it. The skill bridges the gap — it teaches the AI the commands, the conventions, and the guardrails. Together, they turn a series of manual dashboard checks into a conversation.

If you’ve ever spent fifteen minutes bouncing between a payment dashboard, your server logs, and a database query just to confirm a webhook landed, this is the alternative. One terminal, one conversation, same answer.

Try it yourself

The repo is open and includes everything you need:

  • A minimal Bun + Hono app with checkout, webhook, and state tracking
  • The Claude Code skill for the Creem CLI
  • Setup instructions and environment configuration

Install the skill:

npx skills add santigamo/creem-cli-developer-toolkit

Clone the repo, set up your Creem test credentials, start the app, and start talking to Claude Code. The skill handles the rest.

GitHub: santigamo/creem-cli-developer-toolkit

KPI Tracker — AI-Powered Client Performance Dashboard with Notion as the Entire Backend

This is a submission for the Notion MCP Challenge.

What I Built

KPI Tracker is an AI-powered client quarterly performance dashboard where Notion is the entire backend — no traditional database, no SQL, no Redis. Just Notion, connected through the official Model Context Protocol (MCP) server.

The problem it solves: consultants, analysts, and account managers who track multiple clients’ performance currently spend hours manually copy-pasting data from quarterly reports. KPI Tracker reduces that to under 2 minutes.

How it works

  1. Paste a URL or drag-and-drop a PDF — an earnings press release, 10-K, 10-Q, or any financial report
  2. AI extracts all KPIs automatically — Revenue, Gross Margin, Net Margin, EPS, Operating Income, Customer Count, and more
  3. Human-in-the-loop review — you see every extracted KPI with its confidence score before anything saves
  4. One click saves everything to Notion — the app creates a client record, a quarterly report entry, and individual KPI rows via Notion MCP
  5. Live dashboard renders 11+ interactive charts — all reading directly from Notion

Key features

  • Multi-Model AI — choose from 5 models at runtime: Gemini 2.5 Flash, Gemini 2.5 Pro, Claude Haiku 4.5, Sonnet 4.5, or Sonnet 4.6. No restart needed.
  • AI Revenue Forecasting — trend-based next-quarter revenue predictions per client, with reasoning
  • 11+ Chart Types — Revenue Trend (Area), Revenue Forecast (Bar), Margin Trends (Line), EPS Trends (Line), Segment Revenue (Stacked Bar), KPI Distribution (Pie), Confidence Distribution (Pie), QoQ Heatmap, Revenue vs Margins (Composed), KPI Gauges (Horizontal Bar), KPI Landscape (Treemap)
  • Drag-and-Drop Charts — reorder any chart by dragging; order persists in localStorage across sessions
  • Pin KPI Charts — click any KPI in the data table to view its historical trend, then pin it as a new chart on the dashboard
  • Ask AI — query your data in natural language (“Which client had the highest revenue growth?”)
  • Custom KPIs — manually add metrics the AI missed
  • Smart $B Formatting — revenue values ≥ $1,000M auto-display as billions (e.g., $12.7B)
  • All Clients Aggregation — summary cards intelligently aggregate across all clients (Revenue/OpIncome summed, margins/EPS averaged)
  • PDF + URL Ingestion — works with public URLs or local files
  • Secure — API keys never touch the browser; prompt injection detection built-in

Tech stack

Layer Technology
Framework Next.js 15.1 (App Router)
Frontend React 19, Tailwind CSS 3.4
Charts Recharts 2.13 (11+ chart types)
Drag-and-Drop @dnd-kit/core + @dnd-kit/sortable
AI (default) Google Gemini 2.5 Flash
AI (alternative) Anthropic Claude Haiku / Sonnet
Backend/DB Notion (via MCP + direct SDK)
MCP @modelcontextprotocol/sdk 1.27, @notionhq/notion-mcp-server 2.2
PDF Parsing pdf-parse
Deployment Vercel

Demo

Static demo (no API keys needed): notion-kpi.sasrath.com

Show us the code

GitHub logo

sasrath
/
notion-kpi-tracker

Turn any quarterly report URL into a live KPI dashboard. Powered by Claude/Gemini AI + Notion MCP. Zero traditional database.

📊 KPI Tracker — Powered by Notion MCP

AI-powered client quarterly performance tracker with multi-model support (Google Gemini + Anthropic Claude), 11+ interactive charts, AI revenue forecasting, and Notion as the sole backend. Zero traditional database.

Built for the dev.to Notion MCP Challenge

✨ Features

  • 🤖 AI Report Ingestion — paste a URL or drag-and-drop a PDF, AI extracts KPIs automatically
  • 📊 Interactive Dashboard — 11+ chart types (Area, Bar, Line, Pie, Heatmap, Treemap, Composed & more)
  • 🔮 AI Revenue Forecasting — trend-based next-quarter revenue predictions per client
  • 🧠 Multi-Model AI — choose from 5 models: Gemini 2.5 Flash/Pro, Claude Haiku 4.5, Sonnet 4.5/4.6
  • ✋ Human-in-the-Loop — review and edit extracted KPIs before anything saves to Notion
  • 🔄 Drag-and-Drop Charts — reorder dashboard charts by dragging; order persists across sessions
  • 📌 Pin KPI Charts — click any KPI in the table to view its trend, then pin it to the…

The full source is on GitHub: github.com/sasrath/notion-kpi-tracker

Architecture at a glance

User Browser (React SPA)
    │
    ├── Google Gemini API   (web search, extraction, forecasts)
    ├── Anthropic Claude API (extraction, queries, forecasts)
    │
    └── Next.js API Routes
            │
            └── lib/notion-mcp.js (MCP Client singleton)
                    │ (stdio)
                    └── @notionhq/notion-mcp-server (subprocess)
                            │ (HTTP)
                            └── Notion API → 3 Databases

The MCP server runs as a long-lived subprocess managed as a singleton. It starts on the first API call and stays alive for subsequent requests. The callNotionTool() helper handles JSON parsing, error detection, and auto-reconnect.

Key files

  • app/page.jsx — Main dashboard: 11+ charts, DnD grid, KPI table, saving progress, tab persistence
  • lib/notion-mcp.js — MCP client singleton with auto-reconnect and 21-tool mapping
  • lib/llm.js — Multi-model AI layer (Gemini + Claude), safeParseJSON for truncated responses
  • lib/transforms.js — 19 data transform functions for chart preparation
  • setup-notion.mjs — Automated Notion database setup script

How I Used Notion MCP

Notion is not a side feature — it is the backend. Every data operation flows through the official Notion MCP server (@notionhq/notion-mcp-server). There is zero traditional database code anywhere in the project.

The three-database schema

The app uses three relational Notion databases:

Clients — one row per company (Name, Industry, Website, Status)

Quarterly Reports — one row per earnings report, linked to a Client (Quarter, Year, Report URL, Raw Summary)

KPIs — one row per metric, linked to both a Report and a Client (KPI Name, Value, Unit, Quarter, Year, Source, Confidence, Notes)

MCP operations in action

What the app does MCP Tool used
Save a new client, report, or KPI API-post-page
Load all clients / search KPIs API-post-search
Delete (archive) a KPI row API-patch-page
Verify Notion connection on startup API-get-self
Check database schema API-retrieve-a-database

Here’s how the MCP connection is established in lib/notion-mcp.js:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "npx",
  args: ["-y", "@notionhq/notion-mcp-server"],
  env: {
    ...process.env,
    OPENAPI_MCP_HEADERS: JSON.stringify({
      Authorization: `Bearer ${process.env.NOTION_API_KEY}`,
      "Notion-Version": "2022-06-28",
    }),
  },
});

const client = new Client({ name: "kpi-tracker", version: "1.0.0" });
await client.connect(transport);

Every read and write — from loading the dashboard to saving AI-extracted KPIs to deleting a row — goes through this MCP connection. The app treats Notion exactly like a traditional database, but through the standardized MCP protocol.

Why MCP over the direct Notion SDK?

Using MCP means the app communicates through a standardized protocol layer. This gives us:

  1. Tool discovery — the MCP server exposes 21 tools that map to Notion’s REST API; the app discovers them at startup
  2. Protocol standardization — the same MCP client pattern works with any MCP-compatible service
  3. Decoupled auth — credentials flow through the MCP transport layer, not hardcoded in API calls

The result

The entire data lifecycle — AI extraction → human review → save to Notion → read for dashboard → AI forecasting — runs through Notion MCP. No separate database server to manage, no migrations to run, no connection pools to configure. Just Notion.

🔐 Full Demo Access

The public demo at notion-kpi.sasrath.com shows sample data to protect API limits.

For the full live experience with real Notion + Gemini:
👉 notion-kpi.sasrath.com/judges

Pre-loaded with real earnings data from Apple,
Microsoft and Intel Q4 2025.

About Me

I’m a consultant focused on practical AI integrations, data tooling, and building small, maintainable web apps that solve real problems. I publish code, demos, and write-ups so others can learn and contribute — feedback, issues, and pull requests are very welcome.My Github (source: https://github.com/sasrath).

How to Handle Sensitive Data Securely in Terraform

The Three Ways Secrets Leak (And How to Stop Every One)

Day 13 of the 30-Day Terraform Challenge — and today I learned that even when you think your secrets are safe, they’re probably not.

Secrets leak in Terraform in three predictable ways. I found all three. I fixed all three. And I documented what I learned so you don’t have to make the same mistakes.

The Three Leak Paths

Leak Path 1: Hardcoded in .tf Files

The mistake:

resource "aws_db_instance" "example" {
  username = "admin"
  password = "super-secret-password"  # ❌
}

This password is now in your Git history. Forever. Even if you delete it, it’s still in the commit history. Anyone with access to your repo can see it.

The fix:

variable "db_password" {
  type      = string
  sensitive = true
  # No default — Terraform will prompt
}

Now the password never touches your code.

Leak Path 2: Variable Defaults

The mistake:

variable "db_password" {
  default = "super-secret-password"  # ❌ Still in code
}

Default values are stored in your .tf files. Same problem as hardcoding. The secret is right there in version control.

The fix: No defaults for secrets. Ever.

Leak Path 3: The State File

The reality: Even if you fix the first two, Terraform stores every resource attribute in terraform.tfstate in plaintext.

$ cat terraform.tfstate | grep password
"password": "MySecurePassword123!"  # ❌ Right there!

Anyone with read access to the state file can see all your secrets.

The fix:

  • Use remote state with encryption (S3 + encrypt = true)
  • Restrict access with IAM policies
  • Never commit state to Git
  • Enable versioning to recover from mistakes

AWS Secrets Manager: The Right Way

Instead of hardcoding, store secrets in AWS Secrets Manager:

data "aws_secretsmanager_secret" "db_credentials" {
  name = "prod/db/credentials"
}

data "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = data.aws_secretsmanager_secret.db_credentials.id
}

locals {
  db_credentials = jsondecode(
    data.aws_secretsmanager_secret_version.db_credentials.secret_string
  )
}

resource "aws_db_instance" "example" {
  username = local.db_credentials["username"]
  password = local.db_credentials["password"]
}

What this gives you:

  • ✅ Secrets never appear in your .tf files
  • ✅ Fetched at runtime
  • ✅ Centralized secret management
  • ✅ Rotation capabilities

Marking Secrets as Sensitive

Terraform’s sensitive = true prevents secrets from appearing in terminal output:

output "db_password" {
  value     = local.db_credentials["password"]
  sensitive = true
}

Plan output:

db_password = <sensitive>

⚠️ Important: This does NOT prevent secrets from being stored in state. It only hides them from the terminal.

State File Security Checklist

After deploying, I verified all of these:

✅ S3 backend with encryption:

terraform {
  backend "s3" {
    bucket  = "my-terraform-state"
    encrypt = true  # AES-256 encryption
  }
}

✅ Block public access on S3 bucket

✅ Enable versioning to recover from mistakes

✅ Restrict IAM policy to only Terraform runners

✅ .gitignore to prevent accidental commits:

.terraform/
*.tfstate
*.tfstate.backup
*.tfvars

What the Plan Looked Like

When I ran terraform plan, the output showed:

username = (sensitive value)
password = (sensitive value)
db_password = (sensitive value)

The actual secrets never appeared in my terminal. But when I checked the state file:

$ cat terraform.tfstate | grep password
"password": "MySecurePassword123!"

The password was right there in plaintext. This was the “aha!” moment — state encryption isn’t optional. It’s mandatory.

Chapter 6 Learnings

Does sensitive = true prevent secrets from being stored in state?
No. It only hides them from terminal output. Secrets are still in state. You must secure the state file.

Vault vs Secrets Manager:

  • AWS Secrets Manager: AWS-native, simpler, good for AWS-only environments, integrates with RDS
  • HashiCorp Vault: Multi-cloud, dynamic secrets, fine-grained policies, more complex to set up

Why secrets appear in state: Terraform stores all resource attributes to track infrastructure. The only solution is to secure the state file itself with encryption and access controls.

What I Learned

Secrets management isn’t just about not hardcoding passwords. It’s about:

  1. Using a secrets manager — AWS Secrets Manager, HashiCorp Vault
  2. Never hardcoding — no passwords in .tf files, no defaults
  3. Marking sensitive outputs — sensitive = true hides from terminal
  4. Securing the state file — encrypted S3 backend, restricted access
  5. Proper .gitignore — never commit state or tfvars

The state file is the last line of defence. Secure it like you would a password manager.

My .gitignore

# Terraform
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfvars
*.tfvars.json

# Local secrets
*.secret
secrets.tfvars

# Environment
.env
.env.local

The Bottom Line

Three leak paths. Three fixes:

Leak Path Fix
Hardcoded in .tf Use variables with sensitive = true
Variable defaults Remove defaults, use Secrets Manager
State file Encrypted remote backend, restricted access

Security isn’t optional in production infrastructure.

Resources:

P.S. The moment I saw my password in the state file was humbling. No matter how careful you are with your code, if you don’t secure your state, you’re leaving secrets in plain sight.

SAGE — A Notion MCP Academic Co-Pilot for Filipino University Students

This is a submission for the Notion MCP Challenge

What I Built

SAGE — Student Agent for Guided Education — is an academic co-pilot built for Filipino university students. You connect your Notion account, tell it your program, year, and semester, and it builds your entire academic workspace for you: course pages, topic breakdowns, task databases, and study prompts, all structured and ready to go.

It’s built on Notion MCP, FastAPI, Supabase, and Qwen on Vultr Inference. The workspace it creates is seeded with real curriculum data from Ghost Commons, a pre-built registry covering all six BSCPE Year 2 Semester 1 courses — Differential Equations, Engineering Data Analysis, Circuits, and more.

The frustration behind it is real: as a computer engineering student in the Philippines, my tasks come from Teams, Google Classroom, Canvas, and Blackboard all at once. I forget things. I don’t know where to start. SAGE is the one place I wished existed.

Video Demo

Show us the code

github.com/kuya-carlo/sage-mcp

How I Used Notion MCP

Notion MCP is the core of SAGE — not a wrapper, not an afterthought. Every workspace action goes through it: creating the root page, spinning up course databases with full property schemas, inserting topic rows, and querying workspace state for the load monitor.

The hardest part was making it stable at scale. Building a full semester workspace means dozens of concurrent MCP calls — course pages, Topics databases, Tasks databases, individual topic entries — all in parallel. I implemented a global semaphore, persistent SSE connection pooling via an async context manager, and inter-course delays to keep the MCP server stable.

What Notion MCP unlocks here is genuine multi-tenant isolation: every student authorizes their own Notion workspace via OAuth, and SAGE writes only to the pages they explicitly grant access to. Tokens are AES-encrypted at rest. No shared state, no data leakage.

The result is a workspace that feels like it was built by hand — because structurally, it was. SAGE just does it in one shot.

Local-First Software Is Winning: A Developer Guide to Building Without the Cloud

I lost two hours of work last Tuesday because my internet went down for eleven minutes.

Not because I was using some exotic cloud-dependent tool. I was editing a document in a mainstream SaaS product. The kind with a billion-dollar valuation and a “works offline” checkbox somewhere in their marketing materials. When my connection dropped, the app froze. When it came back, my changes were gone. The conflict resolution had decided that the empty server state was more recent than my local edits.

Eleven minutes of downtime. Two hours of work. And a growing suspicion that maybe the way we build software has a fundamental problem.

I am not the only one thinking this. Local-first software, a paradigm where your data lives on your device first and syncs to the cloud second, has been gaining serious momentum throughout 2026. FOSDEM dedicated an entire developer room to local-first, CRDTs, and sync engines this year. Companies like Linear and Figma have proven that local-first principles can power products used by millions. And the developer tooling has finally matured to the point where you do not need a PhD in distributed systems to build this way.

Here is what local-first actually means, why it matters now, and how to start building with it.

What Local-First Actually Means

The term “local-first” comes from a 2019 research paper by Ink & Switch, a lab focused on the future of computing. Their definition is straightforward: local-first software keeps a full copy of your data on your device. The cloud exists for sync and backup, not as the source of truth.

This is not the same as “offline mode.” Most apps treat offline as a degraded state. You get a warning banner. Some features stop working. When you reconnect, the app tries to reconcile what happened while you were offline, and sometimes it gets that reconciliation wrong.

Local-first flips this entirely. Your device is the primary. Reads and writes happen locally, instantly, with zero network latency. The network is secondary. When it is available, your changes sync to other devices and to a server. When it is not, you keep working normally. There is no degraded state because local is the default state.

The seven principles from the original Ink & Switch paper still hold:

  1. No spinners. Operations happen locally, so they feel instant.
  2. Your data is yours. It lives on your device in a format you control.
  3. The network is optional. Everything works offline. Sync happens when possible.
  4. Collaboration works. Multiple people can edit the same data, and conflicts resolve automatically.
  5. Data outlives the software. Your data is stored in open formats, not locked in a proprietary cloud.
  6. Security and privacy by default. Data does not have to leave your device unless you choose to share it.
  7. Full user control. You decide where your data goes and who can access it.

If this sounds idealistic, consider that some of the most successful products of the past few years already follow these principles. Figma handles real-time collaboration with local-first techniques under the hood. Linear stores data locally and syncs in the background. Obsidian keeps everything as plain Markdown files on your machine. These are not niche experiments. They are products that millions of developers and designers use daily.

Why Local-First Is Gaining Ground in 2026

Three things have converged to make local-first practical this year in ways it was not before.

Cloud Fatigue Is Real

The cloud solved enormous problems. It also created new ones. Monthly SaaS bills that compound across dozens of tools. Vendor lock-in that makes switching feel impossible. Outages that take down your entire workflow because a server in Virginia had a bad day. And a growing awareness, especially in Europe with stricter data regulations, that sending every keystroke to someone else’s computer is not always a great idea.

Developers are feeling this acutely. If you are building a product and every feature requires a round trip to a server, your infrastructure costs scale linearly with usage. If you are building local-first, most computation happens on the user’s device. Your server costs drop dramatically because the server is doing sync, not processing.

The Tooling Has Matured

Two years ago, building a local-first app meant rolling your own sync layer. You needed deep knowledge of distributed systems, conflict resolution, and data replication. The developer experience was rough.

That has changed. Tools like PowerSync, ElectricSQL, Zero, and Triplit have turned what used to be a research project into a framework you can npm install. The abstractions are solid. The documentation is real. You can build a local-first app today with the same level of effort as building a traditional client-server app.

Users Expect Instant

We have trained users to expect instant responses. Every major app invests heavily in perceived performance. Skeleton screens, optimistic updates, background prefetching. All of these are workarounds for the fundamental latency of client-server architecture.

Local-first eliminates the need for these workarounds. When your reads and writes happen locally, the UI is genuinely instant. Not “feels instant because we show a skeleton.” Actually instant. Zero milliseconds of latency for every interaction. Users notice the difference even if they cannot articulate why the app “feels fast.”

CRDTs: The Technology That Makes It Work

The hardest problem in local-first is this: if two people edit the same data on different devices while offline, what happens when they reconnect?

Traditional databases solve this with locks or “last write wins.” One person’s changes overwrite the other’s. That is fine for a banking system where you want strict ordering. It is terrible for a collaborative document where both people’s changes matter.

Conflict-free Replicated Data Types, or CRDTs, solve this differently. They are data structures designed from the ground up to handle concurrent edits without conflicts. The math guarantees that no matter what order updates are received, every device eventually converges on the same state. No conflicts. No data loss. No central server needed to mediate.

Here is the simplest way to think about it. Imagine two people have a shared shopping list. Person A adds “milk” while offline. Person B adds “eggs” while offline. When they reconnect, a CRDT-based list will have both “milk” and “eggs.” Neither change is lost, and no conflict resolution was needed. The data structure itself handles it.

This works for more complex scenarios too. CRDT implementations exist for text documents (collaborative editing like Google Docs), JSON-like objects (application state), counters, sets, maps, and more. Libraries like Automerge and Yjs have been battle-tested in production for years and handle edge cases that would take months to solve from scratch.

The practical implication for you as a developer: you do not need to understand the mathematics behind CRDTs to use them. The libraries abstract the complexity. You interact with what feels like a normal JavaScript object, and the sync layer handles replication and conflict resolution behind the scenes.


const repo = new Repo({
  network: [new BrowserWebSocketClientAdapter('wss://sync.example.com')],
})

// Create a document that syncs automatically
const handle = repo.create({ items: [], title: 'Shopping List' })

// Local changes sync to all connected peers
handle.change((doc) => {
  doc.items.push({ name: 'milk', added: Date.now() })
})

That is it. You create a document, make changes locally, and the library handles syncing those changes to every connected device. No manual conflict resolution. No server-side merge logic. The CRDT guarantees convergence.

The Current Tool Landscape

The local-first ecosystem in 2026 has matured significantly. Here are the tools worth knowing about, organized by what they do best.

Sync Engines (Bring Your Own Backend)

PowerSync is the most production-ready option if you have an existing Postgres, MongoDB, or MySQL database. It syncs a subset of your server data to a local SQLite database on the client. Your backend stays the same. You add PowerSync as a sync layer between your existing database and your frontend. This is the lowest-friction path for teams migrating an existing app toward local-first.

ElectricSQL takes a similar approach but focuses specifically on Postgres. It provides active-active sync between your Postgres database and SQLite on the client. If your stack is already Postgres-based, ElectricSQL gives you local-first capabilities without changing your data model.

Full-Stack Local-First Frameworks

Zero and Triplit are designed for building collaborative apps from scratch. They handle the entire stack: local storage, sync, conflict resolution, and real-time updates. If you are building something like a Notion clone or a project management tool, these frameworks give you multiplayer collaboration out of the box.

Jazz takes an opinionated approach to local-first, providing a framework where collaborative data structures are the foundation of your app. It is particularly good for apps where real-time collaboration is a core feature, not an afterthought.

CRDT Libraries

Automerge is the gold standard for document-level CRDTs. It handles complex nested data structures and has excellent support for undo/redo, branching, and merging. If you are building something with rich, structured data (think: a collaborative spreadsheet or design tool), Automerge gives you the most flexibility.

Yjs is the go-to for text editing. It powers the real-time collaboration in many popular editors and has a massive ecosystem of bindings for frameworks like ProseMirror, TipTap, CodeMirror, and Monaco. If your app involves collaborative text editing, Yjs is likely the right choice.

Local Databases

SQLite (via wa-sqlite or better-sqlite3) is the local database that powers most of these solutions. Running SQLite in the browser via WebAssembly has become reliable and performant. On mobile and desktop (Electron, Tauri), native SQLite gives you a proper relational database on the user’s device.

RxDB provides a reactive, local-first database with built-in replication to any backend. It is framework-agnostic and works across browser, Node.js, React Native, and Electron.

When Local-First Makes Sense (and When It Does Not)

Local-first is not the right architecture for everything. Here is a practical framework for deciding.

Local-First Is a Strong Fit When:

Your app handles user-generated content. Note-taking apps, design tools, writing tools, project management, personal databases. Anything where the user creates and owns data benefits enormously from local-first. The data belongs to the user conceptually, so it makes sense for it to live on their device.

Offline capability matters. Field service apps, mobile apps used in areas with poor connectivity, tools for travelers. If your users cannot always count on a stable internet connection, local-first gives them a seamless experience regardless.

Real-time collaboration is a core feature. CRDTs were designed for exactly this. If multiple people need to edit the same data simultaneously, local-first with CRDTs gives you collaboration that works even when participants are intermittently offline.

You want to reduce server costs. When most computation happens on the client, your server does sync and backup rather than processing every request. This can dramatically reduce infrastructure costs, especially for apps with many concurrent users. For indie hackers and solo founders watching their SaaS costs, this matters.

Local-First Is Not Ideal When:

You need a single source of truth with strict consistency. Financial transactions, inventory systems, booking platforms. Anything where two people should not be able to “claim” the same resource simultaneously. CRDTs provide eventual consistency, not strong consistency.

Your data is primarily server-generated. Analytics dashboards, news feeds, search engines. If the server produces the data and the client just displays it, local-first adds complexity without much benefit.

Your app depends on real-time server-side processing. Live video streaming, multiplayer gaming with server authority, real-time bidding. These need a server in the loop for every interaction.

Regulatory requirements mandate server-side control. Some industries require that data be stored and processed on controlled servers. Local-first can complicate compliance in these cases.

Getting Started: A Practical Path

If you want to try local-first without rewriting your entire app, here is a pragmatic approach.

Start with One Feature

Pick a feature in your app where offline capability would genuinely improve the experience. A note editor, a task list, a settings panel. Something self-contained.

Build that one feature with a local-first approach. Use PowerSync or ElectricSQL to sync it with your existing database. Keep everything else the same. This gives you real experience with the paradigm without a risky full rewrite.

Choose Your Sync Strategy

The biggest architectural decision is how data moves between devices. You have three main options:

Server-mediated sync is the most familiar. Clients sync through your server. The server stores the canonical copy and mediates sync between devices. This is what PowerSync and ElectricSQL do. It is the easiest to reason about and fits naturally with existing backend architectures.

Peer-to-peer sync removes the server from the sync path. Devices communicate directly, often using WebRTC. This gives you true decentralization but adds complexity around discovery (how do devices find each other?) and availability (what if the peer you need to sync with is offline?).

Hybrid combines both. Sync through a server when available, fall back to peer-to-peer when not. This gives you the reliability of server-mediated sync with the resilience of P2P. Most production local-first apps end up here.

Handle the Hard Parts

Two things trip up developers new to local-first:

Schema changes. When your data model evolves, you need to handle migrations on every client device, not just on the server. Plan for this from the start. Use versioned schemas and write migration logic that runs locally.

Authorization. In a traditional app, the server checks permissions on every request. In a local-first app, the data is already on the client before the server can say no. You need to think about authorization differently. Typically, the sync layer handles this by controlling which data subsets sync to which clients.

What This Means for the Future of Web Development

I have been watching the JavaScript ecosystem evolve for years, and local-first feels like one of the more significant shifts. Not because it replaces everything, but because it changes the default assumption.

For a decade, the default assumption was: data lives on the server, the client asks for it. Local-first flips that to: data lives on the client, the server helps distribute it.

This is not theoretical. It changes how you think about building products. You stop worrying about API latency because there is no API call for local reads. You stop worrying about loading states because the data is already there. You stop worrying about offline because offline is just the normal state with sync paused.

The ecosystem is still young compared to traditional client-server tooling. You will hit rough edges. Documentation sometimes assumes distributed systems knowledge. Some tools are better suited for greenfield projects than migrations. But the trajectory is clear. Every major sync engine and CRDT library shipped significant improvements in the first quarter of 2026 alone.

If you are building a new product this year, especially anything involving user-generated content or collaboration, local-first deserves serious consideration. The tools are ready. The developer experience is good. And your users will notice the difference the first time they lose internet and your app keeps working like nothing happened.

Resources to Go Deeper

If you want to explore further, here are the starting points I found most useful:

The original Ink & Switch paper (“Local-First Software: You Own Your Data, in Spite of the Cloud”) is still the best introduction to the philosophy. It is long but worth reading in full.

lofi.so maintains a directory of local-first projects, tools, and frameworks. It is the most comprehensive catalog of the ecosystem.

The FOSDEM 2026 Local-First track recordings cover everything from beginner introductions to advanced CRDT implementations. The talks on sync engine internals are particularly good if you want to understand what these tools do under the hood.

And if you want to see local-first in action, clone one of the example apps from Automerge, Yjs, or Zero. Build a simple collaborative todo list. Watch two browser tabs edit the same data. Disconnect one, make changes on both, reconnect, and watch them merge without conflicts. That moment when it just works is when local-first clicks.

The cloud is not going away. But the assumption that every app needs to be cloud-first? That is already changing. And for the developers building the next generation of tools, that shift opens up possibilities that the always-online era never could.

Ritual Protocol: a key as an action, not an object

Ritual Protocol: a key as an action, not an object

Every secret storage system is only as secure as the storage itself. Someone steals your password database — they steal everything. Someone compromises your keystore — they compromise everything. You can encrypt the storage, split keys, add authentication factors — but the fundamental problem remains: the key exists as an object, and objects can be stolen.

Ritual Protocol takes a different approach. The key is not stored. It is reproduced through a sequence of actions — a ritual. Same actions, same parameters — same 32 bytes out. Every time. No storage — no attack vector tied to its compromise.

How it works

A ritual is an ordered set of rites. Each rite has a type and parameters. Order matters: sequences [A, B, C] and [C, B, A] produce different keys.

Rite types

V1 defines six rite types. They can be combined in any order and repeated — each repetition increases entropy.

The rites are intentionally varied — a text string, a symbol sequence, a file slice, a star map, a city and time, runes on a grid. The idea is that every user has their own natural way to create a unique and reproducible ritual. Some will remember a symbol sequence, some will use a file and an offset, some will use a city and time combination with personal meaning.

Full description of each type with entropy formulas is in the specification.

Finalization

And that’s really all there is to it. “I like it. Simple, easy to remember.” — J.S.

Each rite is hashed independently through SHA256, then the hashes are folded sequentially — rite order affects the result, this is an intentional property of the protocol.

The final state is run through three KDFs:

Argon2id (64 MB, t=3, p=4) → scrypt (128 MB) → BLAKE2b-256

Three algorithms were chosen deliberately — each has a different memory access pattern. This makes it harder to build specialized hardware for brute force. Each brute force attempt costs as much memory and time as a legitimate finalization.

At 80 bits of ritual entropy, the effective security is approximately 126 bits — the difference is the computational cost of the KDF chain.

What’s next

The protocol is intentionally minimal — exactly 32 bytes out and nothing more. What to do with them is the environment’s concern. The reference implementation in Go compiles to .dll/.so with a simple C API, which makes it easy to plug the core into almost anything. There’s a pretty decent demo in the examples.

Licensing: the author, as a responsible member of the open source community, hereby contributes this humble piece to the betterment of mankind. Personal use is free. If you’re building a business on it — you know where to find us.

GitHub: https://github.com/runetcom2014/ritual-protocol
Specification: https://zenodo.org/records/19090391