MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

FriendReply AI — Three Natural Ways to Reply to Any Message

2026-10-03 00:30:37

This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend

What I Built

I built FriendReply AI, a small AI-powered web app that helps a friend reply to messages without spending too much time thinking about how to phrase them.

The idea is simple: sometimes you already know what you want to say, but you want a few natural ways to say it.

FriendReply AI lets the user:

  • paste a message
  • choose a tone
  • choose a reply style
  • generate exactly 3 reply suggestions
  • copy the reply they want to use

I built it around a real everyday communication problem and tested the working application with a friend using different messages, tones, and styles.

The goal wasn't to build another general-purpose chatbot. I wanted to build one small tool that does one useful thing well:

Message → Tone + Style → 3 natural replies

Demo

Live Demo

Try FriendReply AI

GitHub

View the source code

The deployed application includes the React frontend and connected backend AI service.

How I Built It

FriendReply AI uses an open-weight AI model through the Dahl inference API.

The architecture is:

React Frontend
      ↓
POST /api/reply
      ↓
Node.js + Express Backend
      ↓
Dahl OpenAI-compatible API
      ↓
Open-weight Model
      ↓
3 Reply Suggestions

Frontend

The frontend is built with:

  • React
  • Vite
  • JavaScript
  • CSS

The user selects a tone and style and sends the message to the backend.

Backend

The backend uses:

  • Node.js
  • Express

The backend handles the AI request instead of exposing the AI API key to the browser.

AI

The application currently uses:

deepseek-ai/DeepSeek-V4-Flash-0731

through Dahl's OpenAI-compatible inference API.

The backend instructs the model to:

  • generate exactly three replies
  • preserve the user's intended meaning
  • avoid inventing facts or commitments
  • follow the requested tone
  • keep replies concise
  • make the suggestions meaningfully different
  • return structured JSON

This makes the AI output easier for the application to process reliably.

Why Does Open Innovation Matter?

Open innovation matters because the AI model should not have to be permanently tied to one closed provider.

For FriendReply AI, the model is separated from the frontend and accessed through a backend inference layer. This means the underlying model can be changed or experimented with without redesigning the entire application.

Using an open-weight model also made this project a useful way to learn how model inference fits into a real application:

User Interface
      ↓
Application Backend
      ↓
Model Inference
      ↓
Structured AI Output

It also keeps the project focused on the application itself rather than requiring a large custom AI training pipeline.

My Agent Session

I did not use DevRelay for this project, so I am leaving the optional agent session section out.

Prize Categories

I am submitting this project for the main Build for a Friend challenge.

Final Thoughts

FriendReply AI is intentionally small.

I didn't want to build an AI assistant that tries to do everything. I wanted to solve one everyday problem for a friend: making it easier to turn a thought into a natural reply.

The project gave me the opportunity to work with an open-weight model, build an AI-backed application from frontend to inference, deploy it, and test the result with a real user.

Thanks for checking out FriendReply AI!

Building a Photo-Based Card Grader: Computer Vision Where the LLM Doesn't Pick the Score

2026-10-03 00:30:01

Trading-card grading looks like an image-classification problem until you try to build it. A grade is driven by several physical properties at once: how centered the art is, how sharp the corners are, whether an edge is whitening, whether the surface has a fold. Each one lives at a different scale in a phone photo.

This is a write-up of how we structured the pipeline behind CardGrade, an app that pre-grades cards from phone photos. Its engine, CGI Vision AI, returns estimated PSA, BGS and CGC grades with confidence scores in about 60 seconds. It's an architecture post, not a benchmark post. I'm not quoting an accuracy number, because a photo-based result is an estimate, and I'd rather say so than dress it up.

The stack, briefly

  • Web: Next.js, React and TypeScript, styled with Tailwind
  • Data: PostgreSQL with Drizzle ORM
  • Mobile: React Native with Expo, using NativeWind for Tailwind-style classes and react-native-vision-camera for capture
  • CV service: a separate Python/FastAPI service running OpenCV
  • Packaging: Docker containers for the web app and the CV service

Why one service owns the pixels

The grading request is small. The web app stores the full-resolution originals and sends the CV service a payload with a grading ID, front and back image URLs, and a webhook URL. The service replies 202, does the work, and calls the webhook with structured results.

The reasoning: the CV service is the only place where the original pixels, the detected card border, and every consumer of the crops meet in a single request. If the web app also cropped images, we'd maintain two implementations of the same geometry, and they would drift.

Step 1: find the card, then flatten it

The first model is a border detector. It finds the card's outer edge and the inner edge of the printed border. Those two quadrilaterals drive everything downstream.

From them we compute a perspective transform and warp each side onto a flat canvas. Small details bite here:

  • Corner ordering must be unambiguous. Top-left minimizes x + y, top-right maximizes x − y, bottom-right maximizes x + y, bottom-left minimizes x − y. Rotated quads are a test case.
  • Warp parameters matter. Bilinear interpolation with replicated borders, so we don't invent edge pixels.
  • Keep a margin. The canvas includes a little background around the card for edge contrast.

Once the card is flat and axis-aligned, zones become slices of one canvas per side: a fixed fraction of the card width for each corner, a thin band for each edge, and the inner art area for surface. Four corners and four edges on two sides gives the 16 inspection zones CGI Vision AI reports on. Centering and surface are assessed alongside them.

Step 2: don't throw away resolution

An early version of the pipeline upscaled small uploads to roughly 4000px on the long edge before doing anything else. The border model was tuned around that scale, and many absolute-pixel thresholds were calibrated to it.

The catch is that upscaling is interpolation, and interpolation is a low-pass filter. Fine scratches are high-frequency detail, and they're gone before a crop is cut. Corner and edge geometry is low-frequency, so it survived. Surface defects didn't.

Removing the upscale outright would change three things at once: the border model's input distribution, the polygon coordinate space, and every absolute-pixel constant. So the V2 cropper uses a two-source model:

  1. Keep the upscaled image only for border geometry.
  2. Map the detected polygons back into original-pixel space.
  3. Cut every defect crop from the untouched original.

The V2 cropper is built around this. It is a Python port of the crop geometry with a versioned geometry registry, checked against the original TypeScript implementation on real-photo fixtures to sub-pixel parity. Zone crops are written under a -v2 naming scheme with a geometry sidecar, so every crop can be traced to the exact geometry that produced it.

Step 3: classical CV for what can be measured

Anything measurable gets measured with OpenCV instead of guessed:

  • Centering comes from border geometry. Average the left and right border widths and the top and bottom widths, express them as percentages (say 48/52), and convert using standard card dimensions of 63.5 × 88.9 mm.
  • Corners and edges get per-zone metrics such as fray, fill, angle, whitening and rounding for corners, and wear measures for edges.

Step 4: a vision model that describes, never scores

For defects that are hard to hand-engineer, a vision model examines each zone crop and describes what it sees, with a severity of none, minor, moderate or severe. The division of labor is strict: the AI never picks scores, it only describes. A deterministic rubric converts those observations and the CV metrics into numbers.

That split means you can trace why a card scored what it did, and changing a scoring rule doesn't mean re-prompting anything. Caps are explicit too: a creasing finding caps the surface score by severity.

Each subgrade uses the weakest zone: corners is the minimum of the four corners, edges the minimum of the four edges, with no averaging. The overall grade is a weighted blend of the four subgrades, rounded to the nearest half point. Centering carries the least weight and surface the most. If centering can't be measured, it is dropped and the remaining weight is redistributed rather than filling in a made-up number. On top of the blend sit weakest-link caps, so one badly damaged pillar can't be averaged away.

Those weights are our model of the process, not anything PSA, BGS or CGC publish. Results are estimates, not official grades, and CardGrade is independent of all three.

The hard part: surface

Surface is where a single phone photo is weakest. Some professional systems build a surface topology map from multiple lighting angles. A phone photo gives you one lighting condition.

Corners and edges have measurement channels. Creases are harder, because a fold is a three-dimensional event and a photo is flat. A model's opinion alone can go wrong in both directions: a real fold can be missed, and a print line can be read as a crease. So crease handling is layered:

  • The vision model describes creasing as one of nine surface families for each face, and it may answer "uncertain" or "unobservable" instead of being forced into "none".
  • A separate surface-defect model can corroborate a crease. A lone moderate-or-worse crease call on an otherwise clean card is checked against it before it is allowed to cost the card.
  • The rubric caps the overall grade by crease severity, because a crease binds the whole card, not just the surface subgrade.
  • Geometry-based crease analysis is part of the toolkit too. It treats a wrinkle (visible on one side) differently from a crease (apparent on both sides), uses cross-side registration as evidence for the more severe class, and treats print lines as the main false-positive class, since a fold changes the surface normal and ink does not.

Mobile notes

The member-facing product is native iOS and Android on React Native and Expo. JS-only changes ship over the air with EAS Update. The runtime version is tied to the app version, so an OTA only reaches matching binaries. When a change alters the client/server contract, keep the server backward compatible with clients that may never update.

What I'd tell someone starting out

  1. Put image processing in one place and keep the app a thin client.
  2. Preserve original pixels for anything high-frequency. Upscale only for what needs it.
  3. Measure what's measurable, and let a model describe only what isn't.
  4. Keep the model out of the final number. A rubric you can read beats a score you can't explain.
  5. Be honest about the ceiling. A photo can't see everything a grader sees under magnification, so the output is an estimate.

If you want to see the result, it's at cardgrade.io. Questions about the pipeline are welcome in the comments.

How to Convert JSON to CSV (Nested Data, Arrays, and Excel Gotchas)

2026-10-03 00:30:00

JSON is great for nested data; spreadsheets want flat rows and columns. Converting between them is easy when your JSON is a list of similar objects, and trickier when it is deeply nested. Here is how to think about it.

The easy case

A JSON array of flat objects maps directly to a table. Each object becomes a row, and each key becomes a column header:

[
  { "name": "Ada", "age": 36 },
  { "name": "Lin", "age": 29 }
]

name,age
Ada,36
Lin,29

Nested objects and arrays

  • Nested objects are usually flattened with dotted column names, so { "address": { "city": "Pune" } } becomes a column called address.city.
  • Arrays are harder. Common choices are to join the values into one cell (red;green;blue), to create numbered columns (tags.0, tags.1), or to make one row per array element.
  • Decide which suits your analysis; there isn't one correct answer.

Quoting and escaping

CSV has a few simple rules (standardized in RFC 4180) that trip up hand-written converters:

  • Values containing a comma, a double quote, or a line break must be wrapped in double quotes.
  • A double quote inside a value is escaped by doubling it: He said "hi" becomes "He said ""hi""".
  • Every row should have the same number of columns.

Inconsistent keys

If objects don't all have the same keys, build the header from the union of all keys and leave missing values blank. Otherwise columns shift and data lands under the wrong heading.

Opening the CSV in Excel

  • Accented and non-Latin characters may look garbled unless the file is saved as UTF-8 with a byte-order mark (BOM).
  • Excel converts things that look like numbers or dates: leading zeros in IDs (00123 becomes 123) and values like 3-4 or 1E5 may be reinterpreted. Import the column as text to prevent that.
  • Some regions use a semicolon as the separator, so check the delimiter if everything lands in one column.

Convert locally

JSON exports often contain customer or business data. Use a converter that runs in your browser so the data never leaves your device.

Frequently asked questions

How do I convert nested JSON to CSV?

Flatten nested objects into dotted column names and decide how to represent arrays, for example by joining values or creating one row per element.

Why does my CSV show weird characters in Excel?

Excel may not detect UTF-8. Save the CSV with a UTF-8 byte-order mark, or import it using the Data tab and select UTF-8.

How are commas inside values handled in CSV?

The value is wrapped in double quotes, and any quotes inside are doubled.

Can I convert CSV back to JSON?

Yes. Each row becomes an object with the header names as keys, though numbers and booleans may need converting from text.

Try it: JSON to CSV — free, runs in your browser, nothing is uploaded.

Originally published at ilovekit.app.

Caching Strategies: Make Backends Fast Without Melting the Database

2026-10-03 00:26:25

⚡ TL;DR: Stop making the database redo the same work a thousand times a second. Compare cache-aside, read-through, write-through, and write-behind, set sane TTLs, and survive the two hard problems: invalidation and cache stampede.

Contents

  • Your database is doing the same work a thousand times a second
  • The one principle behind every cache
  • What a cached request actually looks like
  • The four patterns, and what each one costs you
  • Cache-aside in code
  • The two hard problems: invalidation and stampede
  • Where caches live
  • Common mistakes that cost hours
  • Takeaways
  • Where to go next

Your database is doing the same work a thousand times a second

Picture a product page. Every visitor hits the same endpoint, which runs the same query: "give me product 42 and its reviews." The product hasn't changed in three weeks. Yet your database recomputes that answer thousands of times a second, joining tables, scanning indexes, serializing rows, only to hand back the exact same bytes it handed back a millisecond ago.

That is wasted work, and it is the number one reason backends fall over under load. The fix is not a bigger database. The fix is to remember the answer so you don't have to ask again. That's caching, and getting it right is one of the highest-leverage skills a backend engineer can build.

📌 Who this is for: Junior-to-mid backend engineers who keep hearing "just add a cache" and want to actually understand the patterns, the trade-offs, and the foot-guns. You should be comfortable with HTTP requests and a database query. No distributed-systems PhD required.

The one principle behind every cache

A cache is a small, fast copy of data kept close to where it's needed, traded against the risk that the copy goes stale.

The entire field of caching, in one sentence

Everything else, the patterns, the TTLs, the invalidation headaches, flows from that single trade. You gain speed; you risk serving something slightly out of date. The whole craft is deciding how much staleness you can tolerate for how much speed.

In the real world In tech
The basement archive with every file the company owns Your database, complete, durable, slow to walk to
The handful of folders you keep on your desk The cache, tiny, instant to grab, holds only what's hot
Walking to the basement when a file isn't on your desk A cache miss, fall back to the database, then bring a copy up
A desk folder going out of date after someone edits the master Stale cache, the hard part: knowing when to throw the copy away

A cache is just keeping the files you use most on your desk.

What a cached request actually looks like

The classic flow is the cache-aside read. Before touching the database, ask the cache. If it has the answer (a hit), serve it. If it doesn't (a miss), fetch from the database, drop a copy in the cache for next time, then serve. Here's that path:

A read with cache-aside: hits skip the database entirely; misses populate the cache on the way back.

A read with cache-aside: hits skip the database entirely; misses populate the cache on the way back.

  1. Look in the cache first: Build a stable key like product:42 and ask the cache for it. This is a single fast network round-trip to Redis, typically under a millisecond.
  2. On a hit, serve and stop: If the value is there, deserialize it and return. The database never hears about this request. This is the whole point, the hot path costs almost nothing.
  3. On a miss, fall back to the database: The key isn't cached (first request, or it expired). Run the real query against the database to get the authoritative answer.
  4. Populate the cache with a TTL: Write the fresh value back into the cache with an expiry (say, 300 seconds) so the next reader gets a hit. The TTL is your safety net against staleness.
  5. Return to the client: Serve the answer. The first reader paid the full cost; everyone for the next five minutes rides for free.

The four patterns, and what each one costs you

Reads and writes can each route through the cache in different ways. Four patterns dominate. The split that matters most: does your application code manage the cache (cache-aside), or does the cache layer itself sit in front of the database and manage it for you (read-through / write-through / write-behind)?

Pattern How it works Trade-off
Cache-aside App checks cache; on miss, app queries DB and populates the cache itself. App owns the logic. Simple and flexible, but every reader duplicates the miss logic, and a write must remember to invalidate.
Read-through App only talks to the cache; the cache loads from the DB on a miss transparently. Cleaner app code, but you need a cache that supports a loader, and first-read latency still hits the DB.
Write-through Writes go to the cache and the DB synchronously, in one operation. Cache is always fresh, but every write pays both write latencies, slower writes for guaranteed consistency.
Write-behind Writes hit the cache instantly; the cache flushes to the DB asynchronously later. Blazing-fast writes, but a crash before flush loses data, and the DB is briefly behind reality.

The four core caching patterns. Choose by how much consistency you need and how much write latency you can spend.

For most read-heavy services, cache-aside is the default, it's explicit, easy to reason about, and the cache failing just means slower requests, not broken ones. Reach for write-through when freshness is non-negotiable, and write-behind only when write throughput is the bottleneck and you can tolerate a small window of risk.

Cache-aside in code

Here's the canonical cache-aside read in Python with Redis. Note the three moving parts: a stable key, a TTL on the write, and serialization (Redis stores bytes, not objects).

products.py

import json
import redis

cache = redis.Redis(host="localhost", port=6379, decode_responses=True)
TTL_SECONDS = 300  # 5 minutes, tune to how stale you can tolerate


def get_product(product_id: int) -> dict:
    key = f"product:{product_id}"

    # 1. Ask the cache first
    cached = cache.get(key)
    if cached is not None:
        return json.loads(cached)  # HIT, DB never touched

    # 2. MISS, fall back to the source of truth
    product = db.query_product(product_id)

    # 3. Populate the cache with a TTL so the next reader hits
    cache.set(key, json.dumps(product), ex=TTL_SECONDS)

    return product


def update_product(product_id: int, changes: dict) -> None:
    db.update_product(product_id, changes)
    # Invalidate, don't trust the old copy. Next read re-populates.
    cache.delete(f"product:{product_id}")

The write path is where most bugs live. After updating the database, we delete the cached key rather than trying to overwrite it. Deleting is safer: the next read re-populates from the authoritative source, so you never risk writing a half-built or out-of-order value into the cache.

The two hard problems: invalidation and stampede

There's an old joke that there are only two hard things in computer science: cache invalidation, naming things, and off-by-one errors. The joke is funny because invalidation really is brutal. A cache is a promise that the copy still matches the source, and the moment that promise breaks silently, you're serving wrong data to real users.

Invalidation: knowing when to throw the copy away

You have two tools. TTL (time-to-live) is the lazy, reliable one: every entry self-destructs after N seconds, so staleness is bounded even if you forget everything else. Explicit invalidation is the precise one: when data changes, delete the matching key right then. Real systems use both, a short-ish TTL as a backstop, plus explicit deletes on writes for freshness where it matters.

Stampede: the thundering herd

A cache stampede (or thundering herd) happens when a hot key expires and a thousand concurrent requests all miss at the same instant. They all fall through to the database simultaneously, each running the expensive query the cache was supposed to prevent, and the database, hit with a thousandfold spike, melts. The fix: a short lock so only one request recomputes while the others wait, or staggered TTLs (add a little random jitter) so keys don't all expire on the same tick.

⚠️ A cache is not a database: Caches are allowed to lose data, that's the deal you signed for speed. Never make correctness depend on a value being in the cache. If Redis restarts and forgets everything, your service should get slower, not wrong. Always design the miss path to produce the correct answer on its own.

Where caches live

"The cache" isn't one place. It's a spectrum from closest-and-smallest to farthest-and-biggest, and serious systems layer several:

  • In-process, a hash map or LRU inside your app's own memory. Nanosecond-fast, zero network, but each server has its own copy (no sharing) and it dies on restart. Great for tiny, hot, rarely-changing data like feature flags.
  • Distributed (Redis / Memcached), a separate service all your app servers share. One network hop, survives app restarts, and one invalidation reaches everyone. This is the workhorse for cache-aside. Redis adds data structures and persistence; Memcached is leaner and pure key-value.
  • CDN / edge, caches whole HTTP responses (images, pages, API results) in data centers near the user. The request never reaches your servers at all on a hit. Ideal for public, cacheable content; controlled with Cache-Control headers.

These compose. A request might check an in-process cache, then Redis, then the database, each layer catching what the one before it missed. The closer the layer, the faster and the smaller. Start with one shared Redis; reach for the others when the numbers tell you to.

Common mistakes that cost hours

  1. No TTL. A cache entry with no expiry can outlive its truth forever. If your invalidation has a single bug, you serve that stale value until the heat death of the universe. Always set a TTL as a backstop, even when you also invalidate explicitly.
  2. Caching everything. Caching data that's read once, or changes every second, just adds a network hop and a consistency risk for zero benefit. Cache what's hot and stable, not what's convenient.
  3. Serving stale data silently. Updating the database but forgetting to invalidate the cache is the classic correctness bug. Every write path must either delete or refresh the matching key, make it part of the write, not an afterthought.
  4. Ignoring the thundering herd. A naive cache works fine in dev and stampedes the database the first time a popular key expires under real traffic. Add a lock or TTL jitter to hot keys before they bite you in production.
  5. Trusting the cache for correctness. Treating a cache as durable storage means a Redis restart becomes a data-loss incident. The miss path must always be able to produce the right answer alone.

Takeaways

Caching in nine lines

  • A cache trades a small risk of staleness for a large win in speed.
  • Cache-aside is the sensible default: check cache, miss falls back to DB, then populate.
  • Read-through hides the loader; write-through keeps the cache fresh; write-behind makes writes fast but riskier.
  • Always set a TTL, it's your backstop when invalidation has a bug.
  • On writes, delete the key rather than overwriting it; the next read re-populates cleanly.
  • Cache stampede melts databases, defend hot keys with a lock or TTL jitter.
  • Caches live in-process, in Redis/Memcached, and at the CDN edge; layer them deliberately.
  • Cache what's hot and stable, not everything.
  • Never make correctness depend on the cache, the miss path must stand on its own.

Where to go next

Caching makes reads cheap, but it can't fix a slow query underneath, the first reader and every cache miss still pays that cost. Make the source fast too, and understand how caching fits into scaling the whole system.

Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.

SaaS Cancellation Flow: Show When Access and Billing End

2026-10-03 00:26:14

A clear SaaS cancellation flow tells a customer what stops, when access ends, and whether any charges remain. Show those details before confirmation, then save a visible result that still makes sense after a page reload.

“Cancel subscription” is a short button label. The action behind it may involve a future date, a final invoice, and a workspace that continues to exist. The interface needs to explain those separate outcomes.

Decide what cancellation means in your product

Choose the behavior before writing the confirmation message. Does access end immediately, or at the end of the paid period? Can a customer reverse a pending cancellation? What happens to their workspace afterward?

Stripe’s subscription cancellation guide describes cancellation at the end of a billing period as a way to let customers finish time already paid for. It also describes circumstances where pending invoice items or usage charges still need handling.

That means “You will never be charged again” can be inaccurate. Write copy from the actual billing rules, including outstanding amounts, rather than from the simplest version of the flow.

Put the dates beside the decision

Here is a hypothetical example for a fixed-price product with no outstanding charges. It is sample copy, not a statement about a real subscription:

Cancel renewal? You can use the paid features until November 2, 2026, at 5 p.m. Eastern Time. Your plan will not renew after that. Cancelling the plan does not delete your workspace.

Then give the customer an explicit action such as Cancel renewal, alongside Keep plan.

Use the date and time your product actually applies. A phrase such as “until next month” becomes unclear when someone comes back later. If a final usage invoice may follow, explain that before the decision and link to the relevant billing detail.

Separate a request from a confirmed result

After the customer confirms, show progress while the request is being handled. Do not display a success message simply because the button was clicked.

When the billing service confirms the change, show a durable state in the account page: renewal cancelled, access end date, and any remaining billing information. If undoing cancellation is supported, show that action and its limits there too.

If the request fails, say that renewal is still active. Give the customer a way to retry or contact support. Avoid leaving the screen in a state where the button disappeared but nobody knows whether cancellation worked.

A temporary message can acknowledge the result. It should not be the only place where the customer can find it.

Keep plan cancellation separate from deleting data

Treat ending a paid plan and deleting an account as separate decisions when your product supports separate behavior. Describe what happens to stored work and which features remain available.

If the product removes data after a set period, show that period and any export option before confirmation. If data remains available on a free plan, say which limits apply. Do not promise retention or recovery that your system cannot provide.

An optional reason question can help you learn. Put the actual cancellation action somewhere clear, and let people skip the question. A person who has decided to leave should not need to negotiate with a survey.

Check the flow from the customer's side

Walk through a successful cancellation, a failed request, and a page reload after success. Check that the displayed status matches the billing state in each case.

Also try a second click while a request is in progress. The interface should not create two conflicting actions. Check keyboard access and whether the final status is understandable without relying on a color change.

Finally, ask someone to answer three questions from the screen: Will my plan renew? When does my access end? Is there anything else to pay?

If they cannot answer, improve the explanation before adding more steps. Cancellation clarity comes from matching the words, dates, and saved state to the actual subscription behavior.

Hey I'm Uriel Bitton. I write about building in public strategies and growing startups.

Subscribe for more stories on growing your audience by building in public.

Join us on Buildside: the social network for founders building in public.

Sources

Dadi's Probashi Kitchen: Bringing Home to Expat Friends with Open-Source AI

2026-10-03 00:25:39

 This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend

What I Built

I built Dadi's Probashi Kitchen, a local AI companion designed for my friend Nafis who recently moved from Bangladesh to Melbourne, Australia for his studies.

Living abroad for the first time, Nafis faced severe homesickness and missed traditional homemade comfort food like Kacchi Biryani and Shorshe Ilish. He struggled with two major issues:

  1. Standard Aussie supermarkets don't carry specific local Bangladeshi ingredients (like Ilish fish or local spices).
  2. Family recipes passed down from grandmothers use vague measurements like "ek mutho" (a handful) or " আন্দাজমতো " (to taste), which are impossible for a beginner cook in a new country to decode.

Dadi's Probashi Kitchen acts as a warm, loving Bangladeshi grandmother ("Dadi"). Nafis can type any traditional dish he craves, and Dadi comforts him in "Banglish" (Bengali-English), suggests standard supermarket alternatives available in Western stores (e.g., swapping Ilish for salmon or sea bass), and converts vague traditional measurements into precise cups and grams.

Demo

  • Live App (Render): https://probashi-recipe.onrender.com/ (Note: Hosted on a free-tier server which occasionally runs low on RAM for local LLM extraction. Please see the video demo for local CPU execution!)

When Nafis tested it, he said: "It actually sounds like my Dadi! Replacing the hard-to-find mustard fish with sea bass made making dinner after lectures so much less intimidating."

Code

👵🏽 Dadi's Probashi Kitchen

A locally-run AI assistant built for the Hacktoberfest 2026 DEV Weekend Challenge: Build for a Friend.

The Story

My friend Nafis recently moved from Bangladesh to Melbourne, Australia. He has been incredibly homesick for authentic Bangladeshi food like Kacchi Biryani and Shorshe Ilish, but he struggles to find local ingredients in standard Aussie supermarkets and doesn't understand traditional vague measurements (like "ek mutho" / a handful).

This project solves that problem. "Dadi's Probashi Kitchen" acts as a warm, loving Bangladeshi grandmother. It takes traditional recipe requests, comforts the user in "Banglish," swaps hard-to-find ingredients for Western supermarket alternatives, and converts measurements into exact grams and cups.

Why Open Source?

  1. Cultural Customization: Using an open-weight model allowed me to heavily customize the system prompt to capture the exact warmth, vocabulary, and specific tone of a real South Asian grandmother, avoiding the robotic feel of closed…

How I Built It

The application is built with Python and Streamlit for the user interface, powered entirely by open-source AI:

  • Model: Google's Gemma 2B (gemma2:2b), an open-weight model selected for its lightweight efficiency and strong instruction-following capabilities.
  • Inference: Ollama runs the model locally via CPU inference without requiring paid API tokens or a dedicated GPU.
  • Containerization: The app is fully Dockerized (Dockerfile + start.sh) for deployment options on cloud platforms like Render.
# System prompt snippet defining Dadi's persona in Streamlit
system_prompt = """You are a warm, loving Bangladeshi Dadi (Grandma). Your grandchild just moved abroad and misses your cooking.
1. Comfort them affectionately in a mix of English and Banglish (use sweet words like 'shona', 'bhaiya', 'dadibhai').
2. Adapt recipes using standard Western supermarket ingredients.
3. Convert vague measurements like 'ek mutho' (a handful) into standard cups, grams, or tablespoons."""

Why Does Open Innovation Matter?

Open innovation made two crucial aspects of this project possible that proprietary closed APIs could not deliver:

Cultural Persona Customization without Corporate Censorship or Robotic Tone: Closed commercial APIs often enforce rigid, corporate conversational styles or fail to adopt culturally nuanced dialects like "Banglish". Open-weight models like Gemma 2B allow complete control over system prompts and temperature settings, producing an authentic, comforting persona.

Offline Execution & Complete Recipe Privacy: Family recipes are personal heritage. Running Gemma 2B locally via Ollama means recipes and personal inputs never get sent to cloud logging servers or mined for advertising. Furthermore, because Gemma 2B runs locally on standard laptop CPUs, my friend can run the app offline in his apartment even when his internet goes down—costing $0 to host or maintain indefinitely.

Prize Categories

Best Use of Gemma: Uses Google's open-weight Gemma 2B model as the core engine driving the application.

Best Use of Render: Dockerized and deployed as a web service on Render.