Micronaut 4 application on AWS Lambda- Part 8 Measuring Lambda cold and warm starts with REST API application

In part 6, we learned how to develop a pure Micronaut REST application and deploy it on AWS Lambda.

For the preparation of my talk about developing Serverless Java REST applications on AWS using frameworks such as Micronaut, I found time to measure the performance of the Lambda function with the different approaches (see below). I refer to my articles where we didn’t use the Micronaut REST controllers directly, but the pure AWS Lambda functions, to see how we implemented the SnapStart and priming approaches. They look completely the same when using Micronaut REST controllers.

  1. No SnapStart. Please read the article Introduction to the sample application and first Lambda performance measurements.
  2. SnapStart enabled, but no priming applied. Please read the article Reducing Lambda cold starts with Lambda SnapStart.
  3. SnapStart enabled, and DynamoDB request priming applied. Please read the article Reducing Lambda cold starts with SnapStart and DynamoDB request priming.
  4. SnapStart enabled, and API Gateway request event priming applied. Please read the article Reducing Lambda cold starts with SnapStart and API Gateway request event priming.

Cold (c) and warm (w) start time with -XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation in ms:

Scenario Number c p50 c p75 c p90 c p99 c p99.9 c max w p50 w p75 w p90 w p99 w p99.9 w max
No SnapStart enabled 5496 5646 5894 6239 6378 6383 8.00 9.68 13.51 29.40 68.19 1817
SnapStart enabled but no priming applied, all 2064 2116 2185 2260 4007 4009 8.00 9.23 11.34 23.54 49.64 3201
SnapStart enabled but no priming applied, last 70 2042 2100 2166 2262 2262 2262 8.13 9.53 12.09 24.30 48.86 1742
SnapStart enabled and DynamoDB request priming applied, all 849 911 1856 1892 11951 1951 7.75 8.88 10.41 22.19 52.47 1115
SnapStart enabled and DynamoDB request priming applied, last 70 827 862 923 1151 1151 1151 7.87 9.09 10.66 22.73 48.06 449
SnapStart enabled and API Gateway request event priming applied, all 693 741 1443 1457 1460 1461 8.07 9.54 11.73 22.37 61.04 685
SnapStart enabled and API Gateway request event priming applied, last 70 685 728 773 893 893 893 8.33 10.00 12.30 23.28 52.47 240

What we observe is that both cold and warm start times of the sample application using Micronaut REST controllers are generally higher when using the pure AWS Lambda functions. I suspect the reason for it lies in an extra adapter layer used to convert the Lambda request to the REST controller. This adapter uses several extra dependencies, which leads to a much bigger artifact size, which is also one of the factors of the increased cold start times. The advantage of using the REST controller is the ability to more easily migrate the business logic from AWS Lambda to EC2 or Containers.

Manage Secrets and Environment Variables using dotenvar

Every developer has done it. You’re onboarding a new teammate, the project needs a DATABASE_URL, and instead of setting up a proper system, you paste the connection string in a DM. Fast. Done. Forgotten.

Until it isn’t.

The Real Problem Isn’t Laziness. It’s the Gap.

Raw .env files work — until they don’t. Every developer ends up with their own version. Onboarding takes hours of “hey, what’s the value for JWT_SECRET?” Config drift is invisible. Nobody knows whose copy is canonical. And if one accidentally gets committed, you’re rotating credentials at the worst possible time.

Enterprise secrets managers exist, but they assume a dedicated platform team, a budget, and months of integration work. A startup of five engineers doesn’t need an infrastructure overhaul just to share database credentials safely.

There’s a massive gap between the chaos of ad hoc credential sharing and the complexity of enterprise tooling. That gap is where most teams live. And that’s exactly where dotenvar was built to live.

What Is dotenvar?

dotenvar is the first free, cloud-native secrets and environment variables platform built specifically for developers and engineering teams.

Not a password manager repurposed for .env files. Not a paid enterprise product with a secrets feature bolted on. A platform designed from the ground up for how developers actually work — from local development all the way to production pipelines.

“We built it so we literally can’t see your secrets.”

Every secret you store is encrypted on your device before it ever reaches our servers.

What You Actually Get

Your secrets — organized the way your projects work. Folders, environments, services — everything in one place, clean and structured, accessible to the right people and no one else.

Invite a teammate and they get exactly the access you intended — nothing more. Set it to expire. Revoke it instantly. No credential rotation, no engineering overhead, no coordination required.

Every change to a secret is versioned automatically. Accidentally overwrote something critical? Roll it back in one click. The full history is always there, always encrypted, always yours.

When you’re ready to go beyond the web app, the CLI pulls your secrets into any local project with a single command, or injects them directly into any process without ever writing a file to disk. GitHub Actions, GitLab, Vercel, Docker, Kubernetes — it works everywhere, with any language, any stack.

Why This Matters

Think about how developers managed code before version control. Ad hoc, fragmented, one mistake away from losing everything. Then a platform arrived that made it seamless, collaborative, and reliable — and the entire industry moved forward.

Environment variables and secrets are at that exact same inflection point right now. Every team has the problem. Almost no one has a proper solution — because until now, the proper solution was either too manual or too expensive.

dotenvar is the platform that solves this completely, for every team, at no cost. Versioning. Sharing. Zero-knowledge security. CI/CD integration. All in one place. Free.

Get Started in Two Minutes

No infrastructure. No configuration. No credit card.

dotenvar.com — start now, free.

Documentation · CLI & CI/CD Guide

Tags: Developer Tools DevOps Security Secrets Management CI/CD

Why Most QA Engineers Can’t Practice Their Core Skill — and How Mutation Testing Changes That

There is a strange problem in QA engineering.

If you want to improve as a software developer, you have LeetCode, HackerRank, Codewars. Thousands of problems. Clear scoring. A growing streak to obsess over. You write code, it either passes or it does not, and you learn.

But if you want to improve as a QA engineer — at the actual skill of finding bugs — what do you do?

You can read blog posts about test design techniques. You can study ISTQB syllabuses. You can write tests on personal projects and hope you are getting better. But there is no clear feedback loop. No equivalent of “your solution passed 47 of 50 test cases.” No way to know if you are actually improving at the thing that matters: writing tests that catch real bugs.

That gap is what mutation testing was designed to fill.

The Problem With Practicing on LeetCode

LeetCode is excellent at what it does. It trains algorithmic thinking, data structure fluency, and the ability to write correct implementations under pressure.

But that is not what QA work is.

When a QA engineer sits down with a function like calculate_discount(price, customer_tier), the job is not to implement it. The job is to think: what could go wrong here? What edge cases exist? What assumptions is the implementation making that might not hold? And then — crucially — to write tests that would catch those failures.

LeetCode gives you a specification and asks you to pass it. QA work gives you an implementation and asks you to break it.

These are fundamentally different cognitive skills. One is synthesis. The other is analysis.

Practicing synthesis does not make you better at analysis. And yet, for years, “practice on LeetCode” has been the default advice given to QA engineers who want to sharpen their technical skills.

What Mutation Testing Actually Is

Mutation testing is a technique where small, deliberate changes — called mutants — are injected into working code. Your test suite then runs against each mutant. If your tests catch the bug, the mutant is killed. If your tests all pass anyway, the mutant survives, which means your test suite missed a real defect.

Your score is your kill ratio: the percentage of mutants your tests killed.

Kill Ratio = Killed Mutants / Total Mutants

A kill ratio of 100% means your tests caught every injected bug. A kill ratio of 40% means most of your bugs would slip through undetected.

This gives QA engineers something they have never had before: an objective, repeatable measurement of test effectiveness.

A mutant is not a random or catastrophic change. It is a subtle, plausible defect — the kind a developer might actually introduce. Typical mutations include:

  • Changing > to >= (off-by-one)
  • Replacing and with or in a condition
  • Removing a boundary check
  • Flipping a return True to return False
  • Changing + to - in a calculation

Each one of those is a bug that has appeared in real production systems. Mutation testing forces you to write tests that would catch them.

A Quick Example

Let us make this concrete. Here is a simple discount function:

def calculate_discount(price: float, customer_tier: str) -> float:
    """
    Apply discount based on customer tier.
    - 'gold': 20% discount
    - 'silver': 10% discount
    - All others: no discount
    Returns the final price after discount.
    """
    if customer_tier == 'gold':
        return price * 0.80
    elif customer_tier == 'silver':
        return price * 0.90
    else:
        return price

This is the original implementation. It is correct. Now, a mutation testing system injects a mutant:

# MUTANT: Changed 0.80 to 0.90 (gold tier gets silver discount)
def calculate_discount(price: float, customer_tier: str) -> float:
    if customer_tier == 'gold':
        return price * 0.90  # <-- mutation here
    elif customer_tier == 'silver':
        return price * 0.90
    else:
        return price

This mutant is subtle. The function still runs. It still returns a number. It is the exact kind of bug a tired developer might introduce — and the kind that could cost a business money without triggering an obvious error.

A weak test misses it:

def test_gold_discount():
    result = calculate_discount(100, 'gold')
    assert result < 100  # Too vague — just checks that some discount happened

This test passes against the mutant. The mutant survives.

A strong test kills it:

def test_gold_discount():
    result = calculate_discount(100, 'gold')
    assert result == 80.0  # Exact expected value — catches the wrong discount

This test fails against the mutant. The mutant is killed.

That is mutation testing. You are not testing whether the code runs. You are testing whether your tests can distinguish correct behavior from incorrect behavior.

Why This Matters for Your Career

It Trains the Exact Skill QA Interviews Test

Most QA interviews at some point ask a question like: “How would you test this function?” or “What test cases would you write for a login form?”

What they are really asking is: can you think adversarially? Can you identify the ways this could fail?

Mutation testing practice trains exactly this. When you repeatedly write tests against mutated code and watch your kill ratio go up or down, you start building intuition for which test cases actually matter and which ones are just noise.

After a few dozen problems, you start thinking differently about specifications. You see the boundaries. You see the operator assumptions. You see the edge cases that are easy to miss.

That is what interviewers are looking for — and it is hard to demonstrate if you have never deliberately practiced it.

It Gives You an Objective Metric

One of the perennial challenges in QA is that skill is hard to quantify. Line coverage is widely understood to be a poor proxy. Test count means nothing on its own. “I found 47 bugs last quarter” is not portable across teams or companies.

Kill ratio is different. It is directly connected to the thing that matters: whether your tests catch defects.

A QA engineer who can consistently achieve 90%+ kill ratios on mutation testing challenges has demonstrated something real. That number is not a measure of how fast you type or how well you memorize API syntax. It is a measure of how well you think about failure.

It Builds a Verifiable Portfolio

Most QA portfolio advice is vague. “Contribute to open source.” “Write a personal project with tests.” These are fine suggestions, but they do not produce evidence that is easy for a hiring manager to evaluate.

Mutation testing scores are different. They are objective, reproducible, and specific. A solved challenge at 95% kill ratio with a short explanation of your test design approach is concrete evidence of skill.

It is the difference between saying “I am good at writing effective tests” and being able to show what that looks like in practice.

Try It Yourself

If you want to start practicing, SDET Code is a platform built specifically for this. You can try 3 challenges without signing up — just open the site and start writing pytest. It has 339 challenges across difficulty levels, all focused on mutation testing.

Everything runs in your browser using WebAssembly (no setup, no install), and an AI coach gives feedback on your test design when you want it. It is free to start.

The goal is the same as LeetCode for developers — a deliberate practice environment with clear feedback — but built around the skill QA engineers actually need.

The Bigger Picture

The QA field has a skills measurement problem. We talk about testing principles, but we struggle to create environments where people can actually practice them and get clear feedback.

Mutation testing does not solve every problem in QA. It is one tool, focused on one dimension of test effectiveness. But it fills a gap that has been open for a long time: a way to practice the core adversarial thinking skill of QA work, with an objective score, in a repeatable environment.

If you spend an hour a week on mutation testing problems, you will think differently about test design within a month. The patterns become internalized. The edge cases become automatic.

That is what deliberate practice does. And QA engineers have deserved a proper practice environment for a long time.

This is Part 1 of the “Mutation Testing for QA Engineers” series. Part 2 will cover boundary value mutations and how to develop systematic coverage strategies.

PII-aware routing: how to use cloud AI and keep your sensitive data local

Here’s the tension at the heart of every personal AI system: cloud models are better at reasoning, but your data is private. A self-hosted system can run everything locally — but a 2B parameter model on a mini-PC isn’t going to draft a nuanced email response or analyze a complex financial situation the way a frontier model can.

The naive solutions are both bad. “Send everything to the cloud” means your diary entries, medical notes, and financial records pass through someone else’s servers. “Run everything locally” means accepting worse reasoning on tasks where model quality actually matters.

We built a third option: a PII-aware routing layer that classifies every piece of data by sensitivity, routes it to the right model, and pseudonymizes anything sensitive that needs cloud reasoning power.

The classification: four levels, zero LLM calls

Every record in the system gets a sensitivity level. The classification is entirely deterministic — regex patterns and domain rules. No LLM in the classification loop, because sending data to an LLM to decide if the data is too sensitive to send to an LLM is a circular problem.

Level What it means Example domains
low Public or low-risk data Events, bookmarks
normal Common personal data Notes, contacts, files, diary
high Sensitive personal data Emails, financial transactions
critical Never leaves the device Medical/health data

Each domain has a default sensitivity level. Events are low — knowing you have a meeting at 3pm isn’t particularly sensitive. Emails are high — they contain names, addresses, business context, and sometimes confidential information. Health entries are critical — always local, no exceptions.

But domains are just the baseline. The classifier also scans content for PII patterns that override the default:

Email addresses     → elevate to high minimum
Phone numbers       → elevate to high minimum
Credit card numbers → elevate to high minimum
IBAN codes          → elevate to high minimum
SSN / DNI / NIE     → elevate to high minimum
Medical terminology → elevate to critical

A note titled “Grocery list” stays at normal. A note containing “Dr. García prescribed 20mg omeprazole” gets elevated to critical because the regex matched medical terminology. The content drives the classification, not just the domain.

This is deliberately conservative. The regex patterns over-match — “Dr.” triggers medical detection even if it’s “Dr. Pepper.” False positives mean data gets routed locally when it could have gone to the cloud. False negatives mean sensitive data leaks. Over-matching is the correct failure mode.

The routing decision

Once classified, the router decides where each piece of data goes:

low / normal  → Cloud LLM — best reasoning
high          → Cloud LLM WITH pseudonymization — good reasoning, protected data
critical      → Local model only (Ollama) — or skip if Ollama unavailable

The decision isn’t binary “local vs cloud.” There’s a middle path: pseudonymize the sensitive parts, send to the cloud for reasoning, and de-pseudonymize the response before the user sees it.

This matters because most tasks involving sensitive data don’t need the sensitive parts for reasoning. “Summarize this email thread” needs the content structure and topic — not the actual names and email addresses. “What’s the sentiment of this diary entry?” needs the emotional content — not the specific people mentioned.

The pseudonymizer

When a high sensitivity record needs cloud processing, the pseudonymizer replaces PII with consistent tokens:

Entity type Pseudonym format Example
Person Person_XXXX “Ana García” → Person_A3F2
Email [email protected] “[email protected]” → [email protected]
Phone +00-XXXX-0000 “+34 612 345 678” → +00-E5D9-0000
Organization Org_XXXX “TechCorp” → Org_4C8A
Location Location_XXXX “Calle Sagasta 15” → Location_B2E1

Three properties make this work:

Consistency. The same value always produces the same pseudonym (SHA-256 of the original value, truncated). “Ana García” is always Person_A3F2, in every record, in every session. This means the cloud model can reason about relationships: “Person_A3F2 sent 3 emails to Person_B7D1 about Org_4C8A” preserves the structure even though the names are hidden.

Reversibility. The pseudonym_map table stores every mapping. When the cloud model’s response comes back, the system replaces all pseudonyms with real values before storing or displaying the result. The user never sees Person_A3F2 — they see “Ana García.”

Persistence. Mappings survive across sessions. If “Ana García” was pseudonymized yesterday and appears again today, she gets the same pseudonym. This means the cloud model can build consistent context across multiple interactions without ever learning the real name.

The detection itself uses regex — no LLM call. It’s the same NER-lite approach as the sensitivity classifier: pattern matching for emails, phones, card numbers, and named entity patterns for persons and organizations. Not perfect, but fast and deterministic.

What this looks like in practice

Scenario 1: Calendar event (low sensitivity)

User asks: “What’s on my calendar tomorrow?”

The system fetches tomorrow’s events. Events are low sensitivity. The full data — titles, locations, attendees — goes straight to the cloud model. No pseudonymization needed. The model reasons about the schedule and responds with a natural summary.

Cost: one cloud API call. Privacy: no sensitive data exposed.

Scenario 2: Email analysis (high sensitivity)

User asks: “Summarize the email thread about the partnership.”

The email thread is high sensitivity (default for emails). Before sending to the cloud model:

Original: "Ana García <[email protected]> wrote: Hi Paco, 
regarding the TechCorp partnership with NexaTech..."

Pseudonymized: "Person_A3F2 <[email protected]> wrote: 
Hi Person_0D4E, regarding the Org_4C8A partnership with Org_9F3B..."

The cloud model receives the pseudonymized version. It can still analyze the thread structure, identify that Person_A3F2 is negotiating with Person_0D4E, and summarize the key points. The reasoning quality is nearly identical — the model doesn’t need to know the real names to understand the negotiation dynamics.

The response comes back with pseudonyms:

"Person_A3F2 proposed a revenue-sharing model with Org_9F3B. 
Person_0D4E agreed in principle but requested..."

The system de-pseudonymizes:

"Ana García proposed a revenue-sharing model with NexaTech. 
Paco agreed in principle but requested..."

Cost: one cloud API call + ~2ms pseudonymization. Privacy: no real names or emails left the device.

Scenario 3: Health data (critical sensitivity)

User asks: “What medications am I taking?”

Health entries are critical. They never leave the device, period. The system routes to the local Ollama model. If Ollama is unavailable, the query fails gracefully — it does NOT fall back to the cloud.

The local model’s response might be less polished, but for medical data retrieval, the task is usually simple: find the records and list them. A 2B model handles that fine.

Cost: one local model call. Privacy: absolute — zero data exposure.

Scenario 4: Note with accidental PII (elevated sensitivity)

User creates a note: “Meeting with Dr. López about the lab results. Blood pressure 140/90.”

The note’s domain is normal, but the content contains medical terminology (“Dr.”, “lab results”, “blood pressure”). The classifier elevates it to critical. From this point on, this note is treated like health data — local only.

The user didn’t tag it as medical. They didn’t configure anything. The system caught it automatically. Conservative false positives are the design choice: if a note mentions “Dr. Pepper,” it gets elevated too. That’s a minor inconvenience (one note processed locally instead of on the cloud) with zero privacy risk.

The audit trail

Every routing decision is logged:

Field What it records
domain Which data domain (notes, emails, etc.)
record_id Which specific record
sensitivity Classified sensitivity level
action What happened: sent_pseudonymized, sent_plain, blocked
destination Where it went: embeddings, contextual_retrieval, sleep_time

The pii_routing_log table creates a complete audit of what data was exposed to which processing pipeline. If you ever need to answer “did my medical data ever touch a cloud service?”, the answer is in the log.

This is also how we verify the system works correctly. The log shows every routing decision. If a critical record ever appears with action sent_plain and a cloud destination, that’s a bug — and the log caught it.

Where routing applies

PII-aware routing isn’t just for chat interactions. It applies everywhere the system sends data to an LLM:

Embeddings. When generating semantic embeddings, the text is classified before being sent to the embedding model. If you’re using a cloud embedding API (future option), high and critical records get embedded locally via Ollama instead.

Contextual retrieval. The HyDE pipeline (generating hypothetical answers for better search) uses LLM calls. If the search touches sensitive domains, those calls route through the pseudonymizer.

Sleep-time compute. The background intelligence jobs process records during idle periods. The enrichment job (re-extracting entities from hot records) respects the same routing rules — a critical record only gets re-extracted if Ollama is available.

Entity extraction. When the CRUD hooks pipeline sends text to the LLM for entity extraction, the same classification applies. A health-related note gets extracted locally.

The routing layer sits between every LLM consumer in the system and the actual model call. It’s middleware — invisible to the features that use it, enforced consistently everywhere.

The multi-agent dimension

With a multi-agent topology (7 agents in our system), PII routing gets another layer: agent scoping.

Each agent has a scoped token that defines what domains it can access:

Francis (main):     notes:*, events:*, emails:*, contacts:*, diary:*
Sentinel (infra):   storage:*, hal:*, network:*
Dalí (creative):    photos:read, files:read
Ledger (finance):   finance:*, crm:*
Darwin (analytics): graph:*, insights:*

Sentinel can’t access emails. Dalí can’t read the diary. This is enforced at the API level — even if a prompt injection tricks Dalí into requesting diary entries, the scoped token blocks it.

Combined with PII routing, this creates defense in depth:

  1. Agent scoping prevents access to domains the agent shouldn’t touch
  2. Sensitivity classification catches PII regardless of domain
  3. Pseudonymization protects data that needs cloud processing
  4. Audit logging records everything for verification

A prompt injection attack would need to bypass all four layers to exfiltrate sensitive data. The scoping blocks the API call. The classification catches the content. The pseudonymizer strips the PII. The audit log records the attempt.

What we explicitly don’t do

We don’t use ML for classification. A fine-tuned classifier could be more accurate than regex patterns. But it would need to see the data to classify it — which means sending potentially sensitive data to a model before deciding if it’s safe to send to a model. Regex is dumber but has zero data exposure during classification.

We don’t redact — we pseudonymize. Redaction ([REDACTED]) destroys information the cloud model needs for reasoning. Pseudonymization preserves structure (“Person_A sent an email to Person_B”) while hiding identity. The cloud model can still reason about relationships, quantities, and patterns.

We don’t let the user override critical. You can change a record’s sensitivity from normal to high manually. You cannot downgrade critical to anything else. Health data stays local regardless of user preferences. This is a deliberate paternalistic choice — the privacy risk of accidentally exposing medical data outweighs the convenience of sending it to a better model.

We don’t route based on the LLM provider’s privacy policy. Whether provider A’s privacy policy is better than provider B’s is irrelevant. The system treats all cloud LLMs identically: external services that should never see critical data and should only see high data in pseudonymized form. Trust the math, not the terms of service.

What I’d do differently

I’d add per-field sensitivity, not just per-record. Currently, a contact record is normal even though the phones field is arguably more sensitive than the company field. Per-field classification would let us pseudonymize just the phone number while sending the company name to the cloud. More precise, but also more complex — the pseudonymizer would need to understand JSON field structure.

I’d build a sensitivity dashboard earlier. The pii_routing_log has all the data, but there’s no UI for it yet. A dashboard showing “this week: 450 records processed, 380 sent plain, 65 pseudonymized, 5 blocked” would build user trust and make the privacy system tangible.

I’d make the regex patterns configurable. Different users have different sensitivity needs. A doctor might want “aspirin” to be flagged as medical. A pharmacist might want it treated as normal. The current patterns are one-size-fits-all, which means they’re too aggressive for some users and not aggressive enough for others.

The takeaway

The privacy problem in personal AI isn’t “local vs cloud.” It’s “which data goes where.” Most of your data is fine to send to a cloud model — your calendar events and bookmark titles aren’t secrets. Some data needs protection but can still benefit from cloud reasoning — pseudonymize it and send the structure without the identity. A small fraction of data should never leave your device — route it locally and accept the quality trade-off.

Three components: a regex classifier (zero LLM calls, deterministic), a SHA-256 pseudonymizer (consistent, reversible, persistent), and a routing table (domain defaults + content elevation). No ML, no fine-tuning, no privacy policy trust assumptions.

The system processes your medical notes with a 2B local model and your calendar queries with a cloud model. It knows the difference because a regex matched “blood pressure” — not because it asked an AI what’s sensitive.

Next up: designing an AI approval system — when should your agent ask for permission, and how do you build a confirmation workflow that doesn’t slow everything down?

I built an expense tracker because every other one wanted my bank login

I’ve been tracking my spending since 2008. Back then, the app ecosystem wasn’t really a thing yet. I was living abroad and I just wanted to know where my money was going.

I’ve been in and out of different countries ever since. Through all of it, I needed to track what I was spending. Not budget it, not forecast it, not connect it to my bank. Just write down what I spent so I wouldn’t get a nasty surprise when I checked my balance.

And every expense tracker I looked at over the years wanted something from me before I could start. Create an account. Link your bank. Connect your credit card. Set up a budget. Choose a plan.

I have a credit card and more than one bank account, some of them foreign. Connecting all of that to an app is not “easy setup.” It’s a project. And I didn’t want auto-import anyway. I just wanted to type a number.

The Google Form era

I ended up doing what any reasonable person would do: I made a Google Form.

One form per purchase. It dumped into a spreadsheet. The spreadsheet pulled everything together into months, categories, totals. For seeing the big picture, it was actually great.

For input? Not so much. Every entry meant opening Chrome, filling in every field, even when it was the same location or category as the last one. Currency conversion was a manual job I’d do later, if I got around to it. Too much friction. Easy to skip one entry, then two, then you stop entirely. And once the habit dies, the whole system falls apart.

I used that setup for years. It always kind of worked and kind of didn’t.

What I actually wanted

It came down to one rule: logging an expense should be faster than buying the thing.

Three taps. Open the app, type the amount, hit save. If it takes longer than that, I won’t do it. And if I won’t do it, it doesn’t matter how many features it has.

That constraint shaped almost every design decision. The amount field is focused the second you open the app. You don’t pick a category or location unless you want to. Those fields are hidden by default so they don’t slow you down. After you save, the screen resets and you’re ready for the next one.

Get in and get out. Under 5 seconds.

The currency thing

This is the part that’s personal to me. I live in Thailand. I spend in baht every day. But my brain still thinks in Canadian dollars. A couple times a year I travel and add a third currency to the mix.

Most expense trackers either don’t handle multiple currencies at all, or they handle it badly. You pick one currency and that’s it.

I wanted to log lunch in baht, a hotel in US dollars, and a coffee in euros, and have it all convert back to CAD automatically. Each transaction stores the original amount, the original currency, and the exact exchange rate used. You can always see what you actually paid.

I also built Travel Mode. When you’re abroad and every purchase is in the same foreign currency, you lock that currency so the app defaults to it. Flip it on when you land, flip it off when you’re home. 162 currencies total.

How I built it

Flutter, because I wanted to ship on Android first but keep the door open for iOS. Cross-platform from the start.

For storage, I used Hive, a fast local NoSQL database. Everything is encrypted on-device. There’s no server, no cloud, no backend. I actually built a backend in an earlier version of this project, but it was overkill. So I started over. Keeping it on-device was partly a privacy decision and partly a practical one. Expense data is personal, and I didn’t want to maintain a server.

Offline-first from day one. I travel. I’m not always on Wi-Fi. The app ships with bundled exchange rates so conversions work without internet. When you do have a connection, it fetches fresh rates. But it never depends on one.

I used AI throughout the build. Started with ChatGPT, moved to Claude for most of the heavy lifting. Architecture, debugging, code generation. It’s 2026. If you’re building solo and not using AI, you’re making your life harder than it needs to be.

Six months of solid effort from idea to Play Store.

What’s not there yet

The app doesn’t do everything. No export yet. No backup or restore. No iOS. No receipt scanning. All on the roadmap, but not built.

If you need bank sync, this will never be your app, and that’s fine. There are great options for that. This is for people who want to manually log what they spend, fast, and move on.

Try it

It’s called Enough Spent. Free on Google Play. Right now there aren’t even any ads in it.

If you try it, I’d love to hear what you think. What’s missing? What’s annoying? What would make you keep using it or stop using it? I’m one person building this and real feedback is the most useful thing I can get.

Enough Spent on Google Play

Despliegue, Operación y Troubleshooting interactivo de Infraestructura, usando AWS Kiro

🧠 Introducción

La operación de infraestructura en AWS suele involucrar múltiples fuentes de información: configuraciones distribuidas, logs, métricas, eventos de despliegue y dependencias entre servicios. Este escenario hace que la identificación de incidencias y el análisis de causa raíz (RCA) sean procesos complejos, especialmente en entornos modernos basados en microservicios, IaC y automatización continua.

AWS Kiro surge como una capa de inteligencia que procesa, correlaciona y razona sobre este ecosistema, actuando como un asistente operativo y de troubleshooting interactivo basado en IA, capaz de integrarse desde el despliegue hasta la operación diaria.

📌 Preambulo

¿Qué es Kiro desde el punto de vista de infraestructura?

Kiro puede entenderse como un asistente de troubleshooting interactivo basado en IA, capaz de diagnosticar, correlacionar y resolver incidencias en AWS mediante análisis contextual de múltiples capas:

🔐 Configuración de recursos: IAM, VPC, compute, storage

📊 Métricas y logs: Amazon CloudWatch, CloudTrail

🚀 Eventos de despliegue: CloudFormation, CDK, pipelines CI/CD

🕸️ Topología de arquitectura: dependencias y flujos entre servicios

✔️ En este artículo analizaremos en detalle:

  • Kiro como asistente integral de infraestructura en AWS.
  • Soporte inteligente en el despliegue de infraestructura.
  • Operación asistida mediante observabilidad aumentada.
  • Troubleshooting guiado y análisis de causa raíz (RCA).
  • Cómo Kiro transforma el ciclo completo de infraestructura.
  • Beneficios para equipos de arquitectura y operación.

Despliegue de Infraestructura con AWS Kiro

Una de las principales en los cuales se incorpora Kiro , es por las capacidades avanzadas que apoyan de forma inteligente los despliegues de infraestructura en AWS, ya que permite integración directamente con los flujos de IaC y automatización continua. Su rol en esta etapa es clave para asegurar despliegues consistentes, predecibles y libres de errores.

Validación inteligente de IaC (Infrastructure as Code)
Kiro analiza plantillas y definiciones de infraestructura en formatos como CloudFormation, CDK y Terraform, identificando:

  • Relaciones mal definidas entre recursos.
  • Parámetros inconsistentes o faltantes.
  • Bucles de dependencia.
  • Políticas IAM que no cumplen buenas prácticas.
  • Definiciones que generarán fallos en tiempo de ejecución.

Análisis en tiempo real de eventos de despliegue
Durante un despliegue, Kiro consume:

  • Eventos de CloudFormation Stack
  • Logs de CodeBuild y CodePipeline
  • Salidas de CDK Synth y CDK Deploy
  • Cambios registrados en CloudTrail

Con esta información, puede:

  • Correlacionar errores de compilación con fallos de permisos
  • Detectar recursos que quedaron en estado ROLLBACK_COMPLETE
  • Identificar drifts entre infraestructura declarada y real
  • Explicar qué dependencia o recurso provocó que el despliegue falle
  • Puede guiar paso a paso la solución.

Esto ayuda a detectar errores antes de que lleguen al pipeline.

Prevención de drift y problemas de consistencia
Kiro monitorea continuamente:

  • Configuraciones de IAM
  • Parámetros de VPC y subredes
  • Cambios no declarados en recursos críticos
  • Desvíos entre plantilla IaC y estado real

Si detecta un cambio manual, lo señala, explica el impacto y propone las correcciones para volver al estado deseado.

Acompañamiento guiado en despliegues complejos
Para arquitecturas con múltiples componentes —como EKS, RDS, Lambdas, VPC altamente segmentadas o stacks encadenados— Kiro puede:

  • Proponer el orden correcto de despliegue
  • Verificar dependencias inter-stack
  • Validar prerequisitos (roles, parámetros, networking)
  • Identificar componentes que requieren reprovisión

Esto reduce significativamente el riesgo de fallas por dependencias rotas.

Documentación automática del despliegue
Al finalizar un despliegue (exitoso o con errores), Kiro puede generar:

  • Resumen técnico
  • Recursos afectados
  • Logs relevantes
  • Cambios aplicados
  • Causas raíz de fallos (si ocurren)
  • Pasos a seguir

Esto no solo acelera auditorías, sino que mejora la trazabilidad operativa.

🚀 Asistencia en la Operación de la Reacción a la Prevención

Una de las bondades principales de Kiro, es que actúa como una herramienta de operación predictiva, permitiendo:

– Detección temprana de anomalías
Basado en métricas históricas, distribución de eventos y patrones
operativos.

– Recomendaciones de optimización
Incluyendo:

  • Costos y dimensionamiento.
  • Seguridad (IAM, Security Groups, KMS).
  • Mejoras de rendimiento en componentes compute y networking.
  • Buenas prácticas de arquitectura.

– Documentación automática de incidentes
Genera un informe con:

  • Análisis de contexto.
  • Pasos realizados.
  • Hallazgos.
  • RCA.
  • Solución.
  • Recomendaciones futuras.

Esto reduce drásticamente la carga operativa y mejora la gobernanza técnica.

⚙️ Troubleshooting Interactivo y Basado en Razón

Una de las capacidades más potentes de Kiro es su comportamiento como un asistente técnico conversacional, capaz de:

– Diagnóstico guiado
Kiro analiza el contexto del incidente, revisa logs, revisa configuración, compara con desviaciones previas y propone hipótesis técnicas.

– Correlaciones automáticas
Relaciona métricas de rendimiento con fallos de despliegue, cambios de configuración o eventos de seguridad.

Ejemplos típicos:

  • Un error 503 en API Gateway correlacionado con fallas en Lambda y timeouts de VPC.

  • Caídas en throughput en EKS correlacionadas con cambios de autoscaling o limitaciones de CPU.

  • Incremento en 5xx tras un despliegue específico detectado mediante CloudTrail + CodePipeline.

– Guía hacia la causa raíz (RCA)
Kiro opera como un copiloto técnico:

  • Identifica el punto de falla.
  • Explica qué lo produjo.
  • Muestra el rastro de dependencias.
  • Propone acciones correctivas.

Todo con trazabilidad y evidencia técnica.

🧪 Caso práctico (ejemplo típico)

Escenario: aplicación en EKS con latencia elevada

Kiro permite:

  • Detectar incremento en métricas de latencia (CloudWatch)

Correlacionar con:

  • Saturación de CPU en pods.

  • Throttling en base de datos.

Identificar causa raíz:

  • Subdimensionamiento o mala configuración de autoscaling

Sugerir acciones:

  • Ajuste de HPA

  • Optimización de Queries.

  • Mejora de límites de recursos.

Troubleshooting Interactivo y Basado en Razón

** Aplicación en el ciclo de vida de infraestructura**

👉 Despliegue

  • Identificación de errores en IaC (CloudFormation/CDK)
  • Diagnóstico de fallos en pipelines CI/CD
  • Validación de configuraciones antes de producción

🔄 Operación

  • Monitoreo continuo con correlación automática
  • Detección de anomalías en tiempo real
  • Optimización de performance y costos

🛠️ Troubleshooting

  • Reducción del tiempo de análisis manual
  • Identificación guiada de causa raíz
  • Generación de recomendaciones accionables

📈 Beneficios clave

⏱️ Reducción del MTTR hasta un 40–70%
👥 Menor dependencia de múltiples equipos en incidentes complejos (30–50%)
🔍 Mayor visibilidad end-to-end de la arquitectura
📚 Documentación implícita del proceso de resolución
📉 Disminución de errores operativos repetitivos

💬 Conclusión

Kiro permite diagnosticar y resolver problemas en AWS de forma interactiva, identificando fallos en despliegues, analizando métricas operativas, explicando causas raíz y proponiendo acciones concretas para mejorar la infraestructura, su operación y la documentación asociada a la resolución de incidentes.

Happy learnning on AWS!

Building My Own API Client at 14: From Requests to AI-Powered Insights

**_Building My Own API Client: Why I Did It

I’m 14, and I’ve already spent a lot of time exploring API tools like Postman, Hoppscotch, Insomnia, and more. While they’re all powerful and useful, I wanted something that was truly my own. Using pre-made tools is great, but I often felt limited by their design and workflow. I wanted a tool that reflects how I think about APIs, handle requests and responses, and analyze data in ways that make sense to me. That curiosity and need for control inspired me to start building my own API client from scratch.

What My API Client Can Do

Even at this early stage, the client already supports a lot of features. It handles cookies, request bodies, JSON data, headers, parameters, HTTP methods, and keeps a history of all requests and responses. It also measures request and response size and timing, giving detailed insights into API performance. This makes it easier to understand exactly what’s happening behind the scenes with each API call, instead of just sending requests blindly.

Planned Features and Visualizations

The project is currently about 20% complete, but the roadmap is exciting. I plan to add interactive graphs to visualize request size, response speed, and data capacity, so performance trends are easy to spot. I’m also integrating AI features that can automatically explain responses, suggest smart autofills for requests, and guide users through complex endpoints. The goal is to make API testing and exploration faster, smarter, and more intuitive.

Why Build It at 14

Building a project like this at 14 has been an amazing learning experience. It gives me a head start in understanding web development, API design, and performance optimization. Each bug, limitation, or challenge is an opportunity to improve my problem-solving skills and creativity. Starting early means I can experiment freely, try new ideas, and learn faster than if I waited.

Looking Forward

This API client is more than just a tool—it’s a platform for learning, experimenting, and pushing the limits of what I can create at my age. I’m excited to keep developing it, adding features, improving performance, and eventually having a full-fledged client that I can truly call my own. It’s a journey that’s teaching me so much about coding, design, and problem-solving, and it’s only just beginning._**

How I designed a multi-agent AI support copilot

Every support ticket starts the same way at our SaaS company: open the ticket, scan the description, then spend the next 15 minutes manually gathering context across five different systems. Check application telemetry for exceptions. Look up the customer in the CRM. Grep provisioning logs for failed sync events. Search work item systems and source control for related bugs. Cross-reference identity policy config if authentication is involved.

That context is all out there. It’s just scattered. By the time you’ve assembled it, you’ve already spent most of your time budget on information retrieval, not on reasoning.

That’s what I set out to fix. This is the first post in a series about the multi-agent AI support copilot.

The idea: a side-by-side AI copilot, not a replacement

The typical AI support story starts with a ticket routing chatbot or an automatic responder. That’s not this. We’re not changing the helpdesk agent’s job or asking the model to speak to the customer. Instead, we start a background process alongside the human support agent. The moment a ticket arrives, the copilot gathers context from the surrounding systems and checks whether that evidence corroborates, weakens, or contradicts what the customer is reporting.

The first milestone is deliberately modest from the product side: show the ticket and the supporting evidence side by side so the support agent can reason faster with better context. Under the hood, the implementation already goes further and can synthesize ranked hypotheses with confidence scores, but I still want the first user-visible win to be evidence the human can inspect. For now, the human stays in charge.

Everything that touches the customer still requires a human to approve it.

The design had three hard requirements:

  1. Parallel evidence gathering: all domains queried at the same time, not sequentially
  2. Structured outputs: every agent returns validated JSON, not prose that needs re-parsing
  3. Human-in-the-loop for every action: the copilot informs judgment, humans approve, deterministic code acts

Where the design came from

The initial architecture sketch came from a long ChatGPT conversation, the kind where you brain-dump a problem and the model helps you think through the components. That session produced the five-agent skeleton: identity resolution, observability telemetry, provisioning logs, work items, and a synthesis layer. It also produced the confidence arbitration formula:

FinalConf = sigmoid(Support - Conflict) × AgreementMultiplier

Where Support and Conflict are weighted sums of evidence claims, each claim weighted by the originating agent’s reliability (R_agent) and the claim’s local confidence.

The second source was internal: a reusable agentic scaffolding template we use for experiments. It had a graded architecture philosophy that mapped almost exactly to what we needed:

Grade Template capability What we needed
1 CLAUDE.md session memory SupportAgent persona + IncidentContext schema
2 Domain expertise YAML files Mental models per support domain (APM, CRM, SCIM, B2C, ALM)
3 Skills (SKILL.md with frontmatter) Evidence agent skill implementations
4 Closed-loop validation Re-evidence loop + Reviewer gate
5 Orchestration (parallel worker dispatch) Management Agent with parallel evidence dispatch

The template’s confidence_score field in expertise YAML files turned out to be exactly our R_agent reliability weight. Agents that update their own expertise files after each incident self-improve over time.

The two-layer repo structure

The template introduced a two-layer structure we adopted verbatim:

support-agent-research/
├── .agentic/              ← domain knowledge layer
│   ├── CLAUDE.md          - SupportAgent persona, session bootstrap
│   ├── memory/            - architecture decisions, ADR log
│   ├── plans/             - living docs for major system design changes
│   ├── expertise/         - per-domain YAML mental models
│   └── specs/             - per-incident IncidentContext files
│
└── .claude/               ← execution layer
    ├── agents/            - agent definitions (.md with frontmatter)
    ├── skills/            - evidence skill implementations (SKILL.md)
    └── settings.json      - hook wiring (Reviewer gate, Policy Gate)

The important part is that .agentic/ is the knowledge layer. It keeps the system’s memory between incidents: persona, architecture notes, domain expertise, plans, and incident files. In this article, plans/ just shows that the system’s design and build history live in the same place. .claude/ is the execution layer. It turns that knowledge into agents, skills, and hooks. Keeping them separate means you can update domain knowledge without touching execution logic, and vice versa.

How this matches broader agentic patterns

I didn’t invent this split in a vacuum. The Techorama 2025 sessions I captured in my notebook pointed at the same shape: a central orchestrator, specialist workers, shared context, and a clean split between knowledge and execution. I then turned that shape into a reusable agentic template, basically a starter kit that bootstraps those layers into a repo and keeps the supporting plans, memory, and delivery notes in one place.

That also matches the plan modes now showing up in current CLIs: they separate thinking from doing. The template goes further by preserving state, specialist workers, and feedback loops, so the agent can plan, execute, and improve without starting from zero each time.

These public repos corroborate the same building blocks. microsoft/skills covers skills, custom agents, AGENTS.md templates, and MCP configs. dotagents and source-agents focus on keeping one canonical instruction set synchronized across tools. claude-reflect turns corrections into durable memory and reusable skills. agnix adds validation gates by linting agent configs before they break workflows. For the orchestrator-worker shape itself, I point to the architecture docs from Anthropic and OpenAI below rather than forcing a weak repo comparison.

Anthropic draws a line between workflows and agents and recommends starting simple. The composable patterns they call out, like routing, parallelization, orchestrator-workers, and evaluator-optimizer loops, are the same kinds of building blocks I ended up using here. OpenAI’s Agents SDK makes a similar point by keeping the primitive set small: instructions, tools, handoffs, guardrails, sessions, and tracing. It also separates orchestration done by the LLM from orchestration done in code. That distinction matters to me because I want the model to reason, but I want deterministic code to route work and enforce boundaries. See Anthropic’s “Building effective agents”, OpenAI Agents SDK docs, and OpenAI Agents orchestration docs.

AGENTS.md fits this pattern too. It’s basically a repo-local README for agents, a human-readable place for durable instructions that complements the normal README.md. That’s exactly what .agentic/CLAUDE.md is doing for me here. See AGENTS.md.

.agentic piece What it stores Broader pattern
.agentic/CLAUDE.md Persona, operating rules, bootstrap guidance Repo-local agent instructions, AGENTS.md
.agentic/memory/ Architecture decisions and shared context Durable instructions and session memory
.agentic/plans/ Living record of the system’s design and build history Bootstrap paths, planning, and rollout history
.agentic/expertise/ Domain-specific mental models Specialist workers and routing inputs
.agentic/specs/ Incident-scoped state and evidence Persistent session state, blackboard
.claude/agents/ Worker definitions Handoffs, agent-as-tool
.claude/skills/ Narrow deterministic actions Tools and guardrails
.claude/settings.json Hook wiring and policy gates Deterministic routing and execution control

That’s why I ended up with a central Management Agent, specialized workers, a shared IncidentContext, and deterministic gates. The manager handles routing, the workers stay narrow, and the shared context gives every worker the same incident memory without letting them talk to each other directly. Then the reviewer and policy hooks decide what can actually happen. I want the model to gather evidence and propose answers, but I want code to decide when work is allowed to move forward. That same split shows up in Anthropic’s guidance, OpenAI’s Agents SDK docs, and AGENTS.md.

+------------------------+      +------------------------+
| .agentic               | ---> | .claude                |
| knowledge layer        |      | execution layer        |
|                        |      |                        |
| - CLAUDE.md            |      | - agents/              |
| - memory/              |      | - skills/              |
| - plans/               |      | - settings.json        |
| - expertise/           |      |                        |
| - specs/               |      |                        |
+------------------------+      +------------------------+

Core design decisions

Before writing a line, we laid out the key architectural bets in decisions.md:

  • Orchestrator-Worker over Decentralised: agents never pass control to each other. This prevents circular reasoning and cascading hallucinations. The Orchestrator is the only router.
  • IncidentContext as Blackboard: the shared knowledge base all agents write to and the Synthesis Agent reads from. Enables cross-domain correlation without agent-to-agent communication. “Identity policy changed last week” + “provisioning returned null” + “telemetry shows NullReferenceException” all point to the same root cause.
  • CLI-first auth: every skill uses the vendor’s own CLI (az, gh, acli) for authentication. Skills are credential-free; the bootstrap section validates auth before any query runs.
  • No LLM both concludes and acts: reasoning and execution are separate. The Policy Gate is deterministic code, not a model.

Orchestrator-Worker at a glance

      Support ticket
            |
            v
     Management Agent
   /   |    |    |   
  /    |    |    |    
  v    v    v    v     v
 APM  CRM  SCIM  B2C  ALM
      |    |    |    /
      |    |    |   /
    v  v    v    v  v
     IncidentContext
            |
            v
     Synthesis Agent

IncidentContext at a glance

IncidentContext
  - intake
  - resolved_identity
  - evidence
  - hypotheses
  - audit_log

That’s the design story. In part 2, I’ll show what happened when the first live tickets hit the wiring.

References

How I built cryptographic audit trails for AI agents (and why it matters)

Every company is deploying AI agents. Nobody knows what those agents are actually doing.

An autonomous agent can read your emails, call your Stripe API, export your database, and send messages — all without a human in the loop. When something goes wrong, there’s no proof of what happened, no way to prove authorization, and no compliance trail.
I built MandateZ to solve this. Here’s how it works technically.
The core problem
Traditional software has clear audit trails. An AI agent doesn’t. When a LangChain agent calls send_email(), nothing records: who authorized it, which policy allowed it, what the payload was, or whether a human approved it. That’s fine for demos. It’s a blocker for any enterprise deployment.
The architecture
Everything flows from one data structure — the AgentEvent:

interface AgentEvent {
  event_id: string;        // uuid v4
  agent_id: string;        // ag_ prefix + nanoid
  owner_id: string;        
  timestamp: string;       // ISO 8601
  action_type: 'read' | 'write' | 'export' | 'delete' | 'call' | 'payment';
  resource: string;        // e.g. "emails", "api/stripe"
  outcome: 'allowed' | 'blocked' | 'flagged' | 'pending_approval';
  policy_id: string | null;
  metadata: Record<string, unknown>;
  signature: string;       // Ed25519 signature
  public_key: string;      // agent's public key
}

Every agent action produces one of these. Every event is Ed25519 signed — cryptographically proving which agent produced it and that it hasn’t been tampered with.
Agent identity
Each agent gets a unique Ed25519 keypair on registration:

import { generateAgentIdentity, MandateZClient } from '@mandatez/sdk';

const agent = await generateAgentIdentity();
// { agent_id: 'ag_abc123', public_key: '...', private_key: '...' }

const client = new MandateZClient({
  agentId: agent.agent_id,
  ownerId: 'your_owner_id',
  privateKey: agent.private_key,
  supabaseUrl: process.env.SUPABASE_URL,
  supabaseAnonKey: process.env.SUPABASE_ANON_KEY,
});

Tracking events

await client.track({
  action_type: 'call',
  resource: 'api/stripe',
  metadata: { amount: 9900, currency: 'usd' }
});

Under the hood this: generates an event ID, timestamps it, canonicalizes the payload (keys sorted alphabetically), signs it with the agent’s private key using Ed25519, and inserts it into Supabase. The whole thing takes ~5ms.
Policy enforcement

const result = await client.track({
  action_type: 'export',
  resource: 'database/users',
});
// outcome: 'blocked' — policy rule matched, action never executed

Policies are JSON rules stored per owner. The engine evaluates them before the action executes. Blocked actions are logged with the matched rule ID.
Human oversight gate
For high-risk actions:

const oversight = {
  require_human_approval: ['export', 'delete', 'payment'],
  alert_channel: 'slack',
  timeout_seconds: 300,
  timeout_action: 'block'
}

The agent pauses execution, fires a Slack alert, and waits for human approval. If no response in 5 minutes — auto-blocked and logged.
Why Ed25519 specifically
RSA and ECDSA are too slow for high-frequency agent event signing. Ed25519 signatures are 64 bytes, verification is ~0.05ms, and the keys are small enough to store as base64 strings without bloating your database. libsodium-wrappers handles all of this with a clean API.
The neutrality moat
MandateZ works with LangChain, n8n, AutoGen, CrewAI, and every other framework simultaneously. OpenAI, Anthropic, and Nvidia all have security layers — but only for their own agents. MandateZ is the only layer that works across all vendors without a conflict of interest.
Try it

npm install @mandatez/sdk

Docs: mandatez.mintlify.app
GitHub: github.com/mandatez/core

💊 Did You Take Your Medicine Today? I Built a Pure-Frontend Medication Reminder App

A lightweight, no-sign-up, plug-and-play daily medication reminder tool

🎯 The Origin

As a working professional who kept forgetting to take my vitamins, I realized I needed a simple tool to remind myself to take medications on time. The medical apps out there were too heavy, too complex, and required registration — I just wanted to casually log “did I take my medicine today?”

That’s how this project was born: “Did You Take Your Medicine Today?”

Core Philosophy:

  • ✅ Open and use instantly, no registration
  • ✅ Data stays local, privacy protected
  • ✅ Clean interface, one-tap check-in
  • ✅ Browser push notifications for reminders

🛠️ Tech Stack

Layer Technology
Frontend HTML5 + CSS3 + Vanilla JS
Storage LocalStorage
Reminders Web Notification API
Deployment Static files (hosted on Cloudflare Pages)

Why pure frontend?

  • Zero backend dependency, lowest cost
  • No need to deal with server security or data compliance
  • User data stays completely private, never uploaded to any server

📱 Features

Core Features

  1. Daily Check-in — One tap to mark “I took my medicine today”, never forget again
  2. Medication Management — Add/edit/delete medications, set reminder times and dosages
  3. Browser Notifications — Scheduled push reminders for taking medications
  4. Today’s Status — Open the page and know immediately if you took your medicine today

Additional Features

  • 7-day medication history
  • Adherence statistics
  • Responsive design, mobile-friendly

🎨 UI Design

Using a purple gradient as the primary color, paired with green (success) and red (incomplete) for contrast:

/* Primary color */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

/* Success / Taken */
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);

/* Incomplete */
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);

Design Principles:

  • Large buttons, high contrast, suitable for elderly users
  • Card-based layout, clear hierarchy
  • Max width 480px, mobile-first

💾 Data Structure

{
  "medicines": [
    { "name": "Vitamin C", "time": "09:00", "dose": "1 tablet" },
    { "name": "Calcium", "time": "21:00", "dose": "2 capsules" }
  ],
  "records": {
    "2026-03-27": ["Vitamin C", "Calcium"],
    "2026-03-26": ["Vitamin C"]
  }
}

All data is stored in the user’s browser LocalStorage, never uploaded.

🤔 Technical Challenges

1. Browser Notification Permissions

Many users reject notification permissions on first use. Need to gracefully handle the fallback scenario:

// Request notification permission
Notification.requestPermission().then(permission => {
  if (permission === 'granted') {
    // Can send notifications now
  } else {
    // Show a prompt to guide user to enable manually
  }
});

2. Cross-Date Data Synchronization

Users might add tomorrow’s medication at 23:59. Need to ensure data consistency during date transitions.

3. LocalStorage Capacity Limits

While LocalStorage has a 5-10MB limit, for plain-text medication records this is more than sufficient.

📂 Project Structure

medicine-reminder/
├── medicine-reminder.html   # Main page
├── user.html               # User page
├── login.html              # Login page
├── donation.html           # Donation page
├── MVP-需求文档.md          # Requirements doc (Chinese)
└── _headers                # Cloudflare deployment config

🚀 Deployment

The project is deployed on Cloudflare Pages with a custom domain.

Deployment:

# Install wrangler CLI
npm install -g wrangler

# Deploy
wrangler pages deploy .

📈 Roadmap

Version Features
V1.1 7-day history, adherence stats, data export
V2.0 User accounts, multi-device sync, family reminders

💡 Conclusion

This tool is simple, but it solves a real problem: helping people build the habit of taking medications on time.

On the tech choices, the pure frontend approach sacrifices some “advanced features” (like multi-device sync) in exchange for extremely low development and maintenance costs, plus complete user control over their data. This is a classic “good enough is enough” product decision.

If you have similar needs, feel free to use it or fork it for customization!

Project: [https://github.com/qinyanbo/medicine-reminder.git]

Live Demo: [https://medicine-reminder.shop]

#javascript #webdev #sideproject #productivity #health