LocalFind Gemma — AI-Powered Semantic Search and Chat for Your Local Files

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

LocalFind Gemma is a fully local, privacy-first semantic search engine for your own files — documents, images, and audio — powered by Gemma 4 running on Ollama.

Most search tools match filenames or keywords. LocalFind Gemma understands content:

  • Images indexed by what’s in them — Gemma 4 captions every image at sync time so you can
    search “whiteboard with the system architecture diagram” or “receipt from the coffee shop” and
    actually find it.
  • Agent that reads images to answer questions — ask “how much does that invoice say?” and the
    agent finds the image, sends it to Gemma 4 vision, and gives you the number, not a file path.
  • Audio fully searchable — Whisper transcribes recordings at index time so you can search
    across hours of meetings by what was said.
  • Cross-lingual search — the nomic-embed-text-v2-moe embedding model supports ~100
    languages in a shared vector space. Search in French, find English documents.

Supported file types: PDF, DOCX, TXT, MD, CSV, JPG, PNG, GIF, BMP, WEBP, MP3, WAV, FLAC, M4A.

Everything — Gemma 4, Whisper, the ChromaDB vector store — runs on your machine. No API keys, no cloud, no data leaving your device. There’s also an optional Claude Desktop integration via MCP for files you’re comfortable sharing with a third party.

Demo

Code

https://github.com/maliklovable1-spec/localfind-gemma

How I Used Gemma 4

Gemma 4 isn’t just the chat model here — it’s active at three distinct points in the pipeline:

1. Index time: captioning every image
When you sync a folder, each image is sent to Gemma 4 via Ollama’s vision API. The caption is embedded and stored permanently in ChromaDB. Future searches use the stored caption; the model isn’t called again unless you re-sync. This means fast search without repeated inference.

2. Agent reasoning and tool use
The conversational agent runs on gemma4:e4b (the recommended default). It decides when to search, what query to issue, and how to synthesise results into a direct answer rather than just returning file paths.

I chose e4b over e2b because it follows tool-use instructions more reliably — which matters a lot in an agentic loop where the model needs to decide between search, image reading, and response synthesis. e2b is also supported for users with less RAM (~12 GB vs 16 GB).

3. Live image reading
When the agent finds an image relevant to your question, it sends the image bytes directly to Ollama’s native /api/chat API with your question as context. Gemma 4 reads the image and the agent uses that to answer you. The bytes go from your disk to your local Ollama process —nowhere else.

A note on audio
Gemma 4 E2B and E4B natively support audio transcription at the architecture level — multilingual, up to 30 seconds, built into the model. LocalFind Gemma currently uses Whisper for audio because
Ollama doesn’t expose audio input via its API yet. Once Ollama ships that support
([issue #11798(https://github.com/ollama/ollama/issues/11798)), the transcription backend can
switch to Gemma 4 — the architecture is already designed with that transition in mind, though it will require some code changes depending on how Ollama exposes the audio API.

AI-dy: On-Device Emergency First Aid with Gemma 4

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

Traditional first aid apps often fail during high-stress crises because they bury instructions in dense text or rely on a stable internet connection. Conversely, standard cloud AI chatbots are highly prone to hallucinations, posing a massive safety risk if they invent incorrect medical dosages or dangerous treatments when a user is vulnerable.

AI-dy bridges this gap. It is an offline-first mobile application that combines proactive first aid education with instant, on-device AI assistance. By using spaced repetition (the SM-2 algorithm) in its “Drill Mode,” it helps users build lasting knowledge before a crisis hits.

Built as a Turborepo monorepo, AI-dy combines an Expo 54 (React Native) mobile frontend with a NestJS/PostgreSQL backend for cross-device sync. Crucially, the app bundles 58 lessons across 24 topics as static JSON, meaning the core application and its underlying AI features remain fully operational even with zero network bars.

Demo

Code

AI-dy — On-Device Emergency First Aid Assistant

AI-dy is a mobile-first first aid companion that teaches you when you’re calm and guides you when you’re not. It runs Gemma 4 entirely on your device — no cloud, no connectivity required — so it works in the moments that matter most.

What AI-dy Does

Learn Mode

Structured, searchable first aid education across 58 lessons, 24 topics, and 3 age groups (Adult / Child / Infant).

  • Browse and filter lessons by topic and age group
  • Step-by-step illustrated instructions with Cloudinary-hosted images
  • Watch video demonstrations for key lessons (YouTube, in-app player)
  • Take per-lesson quizzes to test comprehension
  • All content is bundled offline — no network required

Drill Mode

Spaced-repetition practice across 400+ quiz questions using the SM-2 algorithm.

  • Configurable sessions: Quick (5), Standard (10), or Full (20) questions
  • Filter by topic and age group before starting
  • SM-2 scheduling surfaces weak spots first and…

How I Used Gemma 4

AI-dy runs entirely on-device, prioritizing user privacy, speed, and absolute reliability during natural disasters or in rural areas.

Model Selection

I chose Gemma 4 E4B IT with Q4_K_M quantization (~5 GB) executed via llama.rn. The E4B variant is the perfect sweet spot: it comfortably fits within the memory constraints of modern flagship smartphones, yet retains the rigorous instruction-following precision needed for safety-critical interactions that smaller models lack. Users download the multimodal vision projector (mmproj, ~990 MB) during an initial setup to unlock full offline visual capabilities.

How Gemma 4 Powers the Experience

  • Context-Aware Lesson Assistant: When a user asks a question while browsing a lesson, the app dynamically injects the current lesson title and step data into Gemma 4’s context window. This ensures the model gives immediate, hyper-relevant answers tailored exactly to the procedure the user is looking at (e.g., adult burn treatments).
  • Crisis Chat with Multimodal Vision: In an emergency, users can take a photo of an injury. Gemma 4’s vision capabilities analyze the image to identify the type of trauma and guide the user through immediate care steps. If the AI detects that visual context would help, it proactively prompts the user to snap a picture.
  • Voice-First Emergency Navigation: For hands-free operation, a hybrid intent-extraction layer processes voice input. Local keyword matching handles instant navigation for clear emergencies (e.g., shouting “choking!”), while on-device Gemma 4 handles ambiguous natural language inputs—all without a single network round-trip.

Overcoming Technical Challenges

  • Bypassing Latency: Gemma 4’s native reasoning chain can cause a delay. To minimize response latency when seconds count, I utilized the <|channel|>answern marker to skip the “thinking” phase, forcing the model to start streaming the final, actionable response instantly.
  • Stable Vision Inference: To prevent standard context shifting from corrupting critical image data during an interaction, I locked the image embeddings in place by setting ctx_shift: false.
  • Guaranteed Medical Safety: To eliminate the risk of AI hallucinating medical protocols, I enforced a strict architectural boundary. Structured decision trees for medical procedures remain hardcoded and verified; Gemma 4 is strictly limited to intent extraction, navigation, and clarifying context. It can guide you to the right protocol, but it can never “invent” a new one.

What’s Next

  • Medical & Organizational Partnerships: I plan to partner with medical professionals and organizations like the American Red Cross and AHA to continuously validate our content and offer certifiable course tracks.
  • Emergency Services Integration: I aim to build direct dispatch integration into the app. This will allow a user to call emergency services natively while automatically transmitting location data, incident types, and a structured summary of the first aid steps already taken.
  • A Purpose-Built First Aid Model: While Gemma 4 is an incredible general base, our ultimate goal is to fine-tune a specialized, multilingual first aid model on peer-reviewed protocols to optimize safety-critical performance in multiple languages.

Summary

AI-dy demonstrates that powerful, multimodal AI can be a responsible tool in high-stakes domains. By running Gemma 4 E4B entirely on-device, bypassing the thinking chain to cut latency, and constraining the AI to navigation rather than text generation, we’ve built a first aid companion that teaches better in calm moments and thinks faster in a crisis—without ever needing a signal bar to do either.

Team

Datrix: Chat With Your Data Using Gemma 4 — Charts, ML Models, No Code

Hero Image

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

Datrix is a chat interface for your data — upload a dataset, ask questions, get charts and trained ML models back. No code required.
The problem it solves is simple: most people who work with data aren’t developers. They know what they want to find out, they just can’t write the code to find it. Datrix bridges that gap by letting Gemma 4 handle the Python while the user just asks questions.

Interactive charts — ask for a visualization in plain English and get a rendered, interactive chart back in the browser
ML model training — request a trained model, and it gets saved to your session for follow-up predictions in the same conversation
Image input — attach a screenshot of a report or a photo of a whiteboard and ask questions about it alongside your data
Local or cloud — run fully offline with Gemma 4 E4B on Ollama (~9.6 GB RAM, no GPU), or use the 31B model via OpenRouter for heavier analysis

Supported formats: CSV, Excel, JSON, Parquet, and images up to 200 MB.

Hero Image

Demo

Code

datrix-gemma GitHub Repository

How I Used Gemma 4

256K context window

Data analysis is multi-turn by nature — you explore, refine, go back. The 256K context window means Datrix can include the full dataset schema, session charts, and conversation history in every request. Gemma 4 always has the full picture.

Code generation and self-correction

Every query becomes a Python script. Gemma 4 writes it, the sandbox runs it, and if it fails the traceback goes back to Gemma 4 to fix automatically.

Vision

No extra models or OCR setup needed. Images go straight into the Gemma 4 request and get handled natively alongside the rest of the conversation.

Local mode and sensitive data

A lot of datasets people might actually want to analyze — HR records, financials, patient data, internal metrics — can’t go to a cloud API. With Gemma 4 E4B running locally via Ollama, everything stays on your machine. Same experience, fully private, no GPU required.

For users who want stronger reasoning and can’t fit the 31B model on their hardware, OpenRouter is the easy route. But if your machine can handle it, you can also pull gemma4:31b via Ollama and run it fully locally too (requires ~20 GB RAM at 4-bit quantization, 32 GB recommended for comfortable use) — no data leaving your device, full reasoning power.

Understanding Reinforcement Learning with Human Feedback Part 4: Teaching Models Human Preferences

In the previous article, we explored the part where we collect human preferences. In this article, we will see how to use this data to train the models.

To train a model that gives higher scores to preferred responses, we first make a copy of the model that has already gone through supervised fine-tuning.

Modifying the Model

Next, we modify this copied model.

We remove the unembedding layer, which normally predicts the next token, and replace it with a single output value.

The result is a new model called a reward model.

Instead of generating text, this model learns to assign a reward score to a response.

Training the Reward Model

We can now train this reward model using the human preference data we collected earlier.

For a preferred response, we train the model to produce a higher reward value.

For a less preferred response, we train the model to produce a lower reward value or a negative reward.

For example:

  • If humans preferred Response A over Response B, the reward model learns to give a higher score to Response A
  • And a lower score to Response B

Over time, the reward model learns what kinds of responses humans tend to prefer.

We will continue further in the next article

Looking for an easier way to install tools, libraries, or entire repositories?
Try Installerpedia: a community-driven, structured installation platform that lets you install almost anything with minimal hassle and clear, reliable guidance.

Just run:

ipm install repo-name

… and you’re done! 🚀

Installerpedia Screenshot

🔗 Explore Installerpedia here

The Architect’s Pivot: Mastering Parallel Agent Orchestration with Antigravity 2.0

Why the era of inline code completion is over, and how Google I/O 2026’s Gemini 3.5 Flash and Antigravity 2.0 are shifting our roles from writers to orchestrators.

For the past few years, the developer community has treated AI coding tools like sophisticated keyboard extensions. We let them auto-complete our lines, generate standard unit tests, or help us debug an obscure runtime error in a sidebar chat.

​But at Google I/O 2026, Google officially declared this “single-line autocomplete” era over.

​With the release of Gemini 3.5 Flash and Google Antigravity 2.0, we are transitioning from simple AI assistants to something entirely different: autonomous, parallel agent orchestration.

​As developers, this means our primary value is shifting away from syntax generation. The era of the “syntax specialist” is giving way to the era of the Agent Architect.

​Here is what this means under the hood, how it actually works, and a critical look at the new hurdles we need to prepare for.

The New Engine: Gemini 3.5 Flash :
To run complex agentic loops—where an AI writes code, compiles it, reads the error output, adjusts its logic, and tries again—you need two things: exceptional speed and low latency.

​If an agent has to pause for thirty seconds between each step of a self-correcting cycle, the developer experience falls apart. If each self-correcting cycle costs a dollar, the business case falls apart.

Traditional Workflow :

[Task] ──► [Deep Reasoning (30s)] ──► [Compile Error] ──► [Deep Reasoning] ──► (Slow, Costly)

Agentic Workflow :
[Task] ──► [Fast Reasoning] ──► [Compile Error] ──► [Fast Reasoning] ──► (Fluid, Cheap)

Google designed Gemini 3.5 Flash specifically for this recursive loop. By prioritizing agentic execution speeds (pushing an incredible 289 output tokens per second) while maintaining frontier-level intelligence, it provides a model that is fast enough to make multi-step coding agents feel fluid and cheap enough to run hundreds of experimental iterations a day.
By surpassing earlier flagship models on specialized agentic benchmarks like Terminal-Bench 2.1 and MCP Atlas, Google proved that a smaller, optimized engine can actually out-maneuver giant models when it comes to navigating real-world terminal environments and tool-calling structures.

The Command Center: Google Antigravity 2.0
If Gemini 3.5 Flash is the engine, Antigravity 2.0 is the chassis.
​No longer just an experimental IDE plugin, Antigravity 2.0 has been rebuilt as a standalone desktop application designed to coordinate multiple agents working on the same project in parallel.
​The core feature is true parallel multitasking. Instead of asking a single chat window to build your backend and frontend sequentially, you can dispatch specialist agents to run concurrently.

For instance, you can task your Coding Agent with building out a brand-new feature branch while your Asset Agent synchronously generates matching high-quality UI assets and styling tokens.

​Because Antigravity 2.0 decouples agent conversations from a single repository, they operate within a unified project context. They can trace system integration issues across front-end repos, back-end APIs, and infrastructure directories simultaneously, managing separate branches of your local Git repository without ever overlapping or corrupting each other’s active work trees.

​The Orchestration Strategy: Mindset Over Code
​You don’t need a massive enterprise setup to start experimenting with parallel orchestration. But you do need to change how you think about organizing your projects.
​Instead of writing code yourself, you configure your workspace by setting clear guardrails and dispatching tasks conceptually:

​1. Establishing the Guardrails

​Instead of writing explicit code instructions in every prompt, you define a high-level project configuration. You establish the boundaries for your agents: which directories are forbidden, how many agents can run concurrently, and what specific system permissions they hold. This acts as a sandbox, keeping your agentic fleet secure, focused, and aligned with your system boundaries.

​2. Conceptual Dispatching

​Once your guardrails are set, you use simple slash commands to drive your agents.

​Use a command like /goal to let an agent run to completion without constant check-ins, allowing it to autonomously iterate through compile tests and linting. ​Alternatively, use /grill -me to force the agent to ask clarifying questions about your system architecture before it touches a single file, ensuring human alignment before execution begins.
​While you step away to focus on higher-level design, your local workspace coordinates these loops in the background—balancing code generation, verification, and asset outputs simultaneously.

​The Thought-Provoking Reality: The New Bottlenecks
​While the promise of Antigravity 2.0 is incredible, we have to look past the keynote demos and address the practical friction points this paradigm creates for everyday software engineers.

  • The Verification Tax

​If an agent can write, compile, lint, and test 1,000 lines of code across 15 files in two minutes, who is responsible for reading it?
​Historically, we spent 80% of our time writing code and 20% reviewing it. In a multi-agent world, that ratio flips entirely. Our job becomes 100% auditing.
​Reading and verifying foreign code is often much harder than writing it yourself. It requires deep, continuous focus to spot subtle logic gaps, security holes, and architectural drifts that automated compiler checks might miss. If we simply accept pull requests because “the tests passed,” we will inevitably build fragile, unmaintainable software.

  • The Identity Crisis
    ​As engineers, a lot of our personal pride is tied to the craft of writing code—the elegant abstraction, the clever algorithm, the satisfaction of solving a syntax puzzle.
    ​When autonomous agents do the heavy lifting, we must redefine what it means to be a “developer.” We are no longer the typists. We are the curators, directors, and architects. Embracing this pivot requires a shift in mindset: measuring our success not by the lines of code we write, but by the integrity of the systems we orchestrate.

​Conclusion

​The announcements at Google I/O 2026 are a clear signal of where the industry is going. Autocomplete is quickly becoming yesterday’s technology.

​By combining the speed of Gemini 3.5 Flash with the parallel workspace mechanics of Antigravity 2.0, Google has laid the groundwork for a new way of building software. The future belongs to the engineers who learn how to write precise guidelines, configure resilient agent platforms, and manage complex system architectures.

Quidditch – Powered By PostgreSQL and ASP.NET

As a software developer, it’s important to work on your craft. That’s why I’ve been studying ASP.NET for the past few months. In my last article I walk through how to set up an ASP.NET Minimal Web API. Today, I’ll be building on that foundation, by adding a persistent data store, a la PostgreSQL. In order to configure a .NET API to communicate with a PostgreSQL database, I’m going to use Entity Framework.

Though I could have worked with my animal sounds API, I decided to implement the database for an app that scores a road trip game that my wife and I took this summer.
I recently saw a video about a new road trip game that’s inspired by Harry Potter (my favorite book series). I knew that my wife and I had to try it. If you’re interested, the rules of the game are described in this video, and you can play using my score tracker at https://quidditchtrip.nolanmiller.me.

In this article, I’ll go over making sure that you have PostgreSQL installed and running, and then how to install and configure Entity Framework to read to and write from the database.

What you’ll need to start:

  • A .NET Web API (that you’ve created using the dotnet CLI)
  • Access to an internet connection
  • A little bit of patience

Install Postgres

There are a few different methods to installing and starting a Postgres server on your machine, but the easiest route is downloading Postgres.app, a MacOS postgres server interface that comes bundled with the psql CLI. If you’re not on Mac, or if you’re interested in other installation methods the official Postgres website has installation instructions for every operating system and includes instructions for other installation methods.

Once you’re done you should be able to start your Postgres server and also connect to it using the psql command line interface.

Create a database

Before we can start creating our models and writing data, we need to make sure that we have a database for our application. To do this, connect to psql using the following command:

psql -U postgres

This will connect us to the Postgres server with the default user postgres. If it prompts you for a password, it should be the default password, postgres.

Once you’re connected you should see a prompt that ends with # and we can issue the following SQL command:

postgres=# CREATE DATABASE Quidditch;

Since this database is for my Quidditch app, I’ll name it Quidditch, just replace this with whatever name makes sense for your application.

Connect to your database

With a database set up, it can be connected to our .NET application using Entity Framework, an object-relational mapper for the .NET ecosystem. EF will automate reading and writing data on our database, and handle table creation and modifications through the development process.

To get started with EF for Postgres, install two NuGet packages by running the following commands from a terminal in your .NET project:

dotnet add package Microsoft.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design

To interact with a database we have to create a database context to allow us to interface with the database, configure the context object to use the proper connection string, set up models and create a migration using EF that will scaffold our database.

1. Database Context

To create a database context, create a class that inherits from EF’s DbContext class, like so:

using Microsoft.EntityFrameworkCore;

public class QuidditchContext : DbContext
{
    public QuidditchContext(DbConextOptions<QuidditchContext> options) : base(options) { }
}

Then wire it into your Program.cs file.

// in Program.cs
var connectionString = "[Your connection string]";
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<QuidditchContext>(options => options.UseNpgsql(connectionString));

This way of setting a connection string isn’t secure, and should only be used for testing. Make sure that you load these in via a config file!

And with these two small additions, Entity Framework can access your database server.

2. Set up models

The next step in configuration is defining the models that we want to persist. For my Quidditch game, since it’s a simple score tracker, I really only need to capture two data models: games and teams. Instead of creating two database tables for these and setting up their columns, I can just create the model in my C# code and EF will reason out the structure of the database!

public class Team
{
    public int TeamKey { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedDateTime { get; set; }
    public int? GameKey { get; set; }

    [JsonIgnore]
    public Game Game { get; set; }
}

public class Game
{
    public int GameKey { get; set; }
    public DateTime GameStartDateTime { get; set; }
    public DateTime? GameEndDateTime { get; set; }
    public bool IsActive { get; set; } = true;
    public bool IsFinished => GameEndDateTime != null;

    public List<Team> Teams { get; set; } = new List<Team>();
}

A couple of things to point out here. First, I’m using the terminology [Entity]Key to define my primary keys. This is not automatically picked up by EF. They would prefer that you use [Entity]Id instead. Unfortunately, I am stubborn and would have been mistyping “Key” instead of “Id” of for the whole project, so I took the extra couple of steps to define my keys and relationships.

When I add these models to my database context in the next step, EF will use these models to set up tables and columns based on them. Not everything will be one for one though. There are two special properties here. The Team.Game property and the Game.Teams properties are called Navigations.

A Navigation property models a relationship between two tables. They work by associating related models using primary and foreign keys on the models and are kept in sync by EF.

Before these will behave as expected, we need to register these models in our DbContext with DbSet properties. This can be done as easily as adding properties to any other class, but set the type to DbSet<[Entity]> where [Entity] is the model that you want to register.

// QuidditchContext
public class QuidditchContext : DbContext
{
    public QuidditchContext(DbConextOptions<QuidditchContext> options) : base(options) { }

    public DbSet<Team> Teams { get; set; }
    public DbSet<Game> Games { get; set; }
}

The DbSet<> class comes from Entity Framework and inherits from IEnumerable<T>, which allows us to use LINQ (and some Entity Framework specific versions of LINQ) to access our models.

Since I decided to use a different convention for my primary and foreign keys, I have another configuring step to define the relationships between my tables. To manually define table and column relationships, override the OnModelCreating method in DbContext. The relationships that I used are below.

public class QuidditchContext : DbContext
{
    // ...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // .HasKey tell EF what the name of the primary key should be
        modelBuilder.Entity<Team>()
            .HasKey(t => t.TeamKey);

        modelBuilder.Entity<Game>()
            .HasKey(g => g.GameKey);

        // These methods define the one-to-many relationship in
        // what looks like plain language. 
        modelBuilder.Entity<Team>()
            .HasOne(e => e.Game)
            .WithMany(e => e.Teams)
            .HasForeignKey(t => t.GameKey)
            .OnDelete(DeleteBehavior.SetNull);
        // .OnDelete can set what happens to related tables
        // when the record they reference is deleted.

        // I don't want a table to be created for this model.
        modelBuilder.Ignore<LeaderboardEntry>();
    }   

Even if you’re not using custom naming for your properties, it is probably a good idea to override this method so that your complex relationships are explicitly set by you. This will help mitigate unexpected behavior and has the added benefit of providing some in-code documentation.

Now that configuration is done, it’s time to create a database through a process called migration.

3. Create a migration

As you work on a project, inevitably the shape of your data will change here and there. Instead of manually updating your database tables, Entity Framework handles this through migrations.

Since the models are already defined in the code, Entity Framework can scaffold and re-scaffold database, throughout the lifecycle of the application. A migration can be created using the dotnet CLI with the following command.

dotnet ef migrations add InitialCreate

With dotnet ef the Entity Framework-specific tools are specified, and then using the migrations add command a migration is created called InitialCreate. A migration can have any name, but it’s a good idea to give it a descriptive title for when you need to roll your changes back.

To finalize the changes on our database (or “creation of” in this instance) we still have to run one more command. EF separates this process of updating the database into two steps. In the first step, a few things happen:

  • a Migrations directory is created in the root of your project if not already there
  • a [TIMESTAMP]_InitialCreate.cs file is generated, containing the steps to implement the changes to your context, and steps to walk it back if needed.
  • a [TIMESTAMP]_InitialCreate.Designer.cs file is generated with metadata for EF to use.
  • a MyContextModelSnapshot.cs file containing a current snapshot of your context model is created.

The syntax in these files is fairly easy to interpret, so it’s a good idea to do a once-over of the Up method in the [TIMESTAMP]_InitialCreate.cs method so that you aren’t surprised by any changes!

Now to finalize those changes and apply them to the database run the following command.

dotnet ef database update

This will updates the database to the most recent migration. Be careful as you do this, there is potential for data loss when running this command! Now, the database and it’s tables are set up and ready for you to start querying!

Using LINQ to access your data

To illustrate how to query a database using Entity Framework, take a look at one of the service methods that I created for my application:

public async Task<ServiceResponse<Game>> GetGameByKey(int gameKey)
{
    var game = await _context.Games
        .Include(g => g.Teams)
        .SingleAsync(g => g.GameKey == gameKey);
    if (game == null) { return ServiceResponse<Game>.Failure("..."); }
    return ServiceResponse<Game>.Success(game);
}

If you’re familiar with LINQ syntax, then this should be familiar.

public async Task<ServiceResponse<Game>> GetGameByKey(int gameKey)

First, I’m declaring a method called GetGameByKey, that returns a custom response object with the generic type Game. I just created a response class with a generic Payload property so that I can send back metadata along with the requested models if needed.

var game = await _context.Games
        .Include(g => g.Teams)
        .SingleAsync(g => g.GameKey == gameKey);

This line queries the database, translating the database record into a usable model and assigning it to a variable that we can return from the method! By default, navigation properties are not included, but using .Include() and specifying the navigation property includes the related navigation in the query.

Query execution is signaled by the .SingleAsync() method, which works the same way as .Single() in LINQ. So this query, will return a only a single record where the GameKey column’s value is equal to the gameKey argument passed in when this method is called.

Here’s another method example:

public async Task<ServiceResponse<Game>> SetGameInactive(int gameKey)
{
    var gameResponse = await GetGameByKey(gameKey);
    if (!gameResponse.WasSuccessful) { return gameResponse; }
    var game = gameResponse.Payload;

    game.IsActive = false;
    _context.Games.Update(game);
    var changedRows = await _context.SaveChangesAsync();
    if (changedRows < 1) { return ServiceResponse<Game>.Failure("Failed."); }

    return ServiceResponse<Game>.Success(game);
}

There’s a little more logic going on in this method. The goal is to find the Game with the matching GameKey and then set its IsActive property to false. The Game is retrieved in the same way as the first method we looked at. Once it’s been accessed, then it’s properties can be updated like any other class instance in C#.

The _context.Games.Update() method prepares EF to send the command to the database. Unlike the first query, the execution is started by SaveChangesAsync(), which returns the number of affected rows. This is the EF pattern for modifying existing data. The changes are made in the code, and SaveChanges() or SaveChangesAsync() must be called.

I hope that this shows just how simple it can be to access the data from a database using familiar C# list patterns. For a full list of methods available to Entity Framework, check out their documentation.

Summary

If you made it this far, thank you for reading! I hope that you found it helpful and I wish you the best of luck in implementing EF Core into your own C# applications.

This was a unique challenge for me. I primarily write in C# for my job, but we use a custom data access library that requires mapping our models manually. While Entity Framework has some nice quality-of-life features, I found that lose some of the flexibility gained by writing your own SQL.

This was a very simple project, but even still, it felt like I didn’t save much time by using an ORM. Rather than save time on having to write SQL, I traded that time for configuring my database and tables in C#.

In my day job, I rely on libraries that have been provided for me, so I’m looking forward to learning how to implement database connections on my own. I don’t know that I would choose to use Entity Framework again. I liked how easy it was to set up my models and navigations made implementing business logic very nice, but the configuration of relationships just wasn’t as straightforward as it is in SQL.

Regardless, it is always good to take some time to learn new technologies.

Thanks again for reading!

Build a Database Connection Framework In 133 Lines Of Code

Entity Framework is a popular database connection choice for .NET developers. It’s fairly simple to use but, what if I told you that we could create a connection framework on top of ASP.NET that would allow us to get total control of the SQL that we write? The cherry on top is that it will take about as much code as it takes to configure Entity Framework.

In this article, I’ll show you how to connect an ASP.NET minimal API to a local SQLite database Entity Framework. We will still map tables into classes allowing us to interact with our data in C#. I’ll include every line of code you need, so let’s get started.

The data

First, we have to do is set up a database. I decided to use SQLite since I’ve never had an excuse to before. Make sure that you have SQLite installed and that you’ve connected to a database. (You could also connect to a MySQL, SQL Server or Postgres database using the following method too, but this article will focus on SQLite.)

With our database, we’ll track student information and grades. First, connect to SQLite and create a database, we’ll call it Students.db, then create a students table and a grades table.

sqlite3 Students.db
sqlite> CREATE TABLE students (
...>        id INTEGER PRIMARY KEY,
...>        name TEXT,
...>        school TEXT
...> );
sqlite> CREATE TABLE grades (
...>        id INTEGER PRIMARY KEY,
...>        scored INTEGER,
...>        out_of INTEGER,
...>        student_id INTEGER,
...>        FOREIGN KEY (student_id) REFERENCES students(id)
...> );

Drop these two table definitions into ChatGPT and have it script you out some sample data for both of the tables. This isn’t necessary, but it will make your API more interesting to work with once we’re done. Once you’ve done that, we’re ready to get started.

The connection

Instead of using Entity Framework, we’ll remove that layer of abstraction to use ADO.NET. These are the libraries that EF uses in its implementation. Microsoft was kind enough to wrap them up in a NuGet package for us. In the root of your project, run the following command:

dotnet add package Microsoft.Data.Sqlite

With that installed, we can work on getting our application connected to the database. First, grab your connection string. Each database provider has their own format for these, but I’ll trust that you can find that and configure it on your own. Sqlite’s format is below:

"Data Source=path/to/database_file.db"

The best way to give your application access to this is through a configuration object that you supply through dependency injection. I wrote an article about how to get that set up if you need help. Or you can decide to be a lawless cowboy and hard-code it… I’m not your mother.

It’s finally time to write some code. In order to interact with our database, we need a class and a matching interface that will provide the connection to the rest of our application. We will call it, somewhat unimaginatively, DatabaseConnectionProvider.

// DatabaseConnectionProvider.cs
using Microsoft.Data.Sqlite;

namespace Students.Repository.SQL;

public class DatabaseConnectionProvider : IDatabaseConnectionProvider
{
    private readonly string _connectionString;

    public DatabaseConnectionProvider(IConfiguration config)
    {
        // Don't you hard code it, cowboy...
        _connectionString = config["ConnectionString"]!;
        if (string.IsNullOrEmpty(_connectionString))
        {
            throw new InvalidOperationException("Connection string not found.");
        }
    }
}

Since we’ll be injecting this provider into other classes later, we’ll create an interface and include a preview of the first method that we will create!

// IDatabaseConnectionProvider.cs
namespace Students.Abstractions;

public interface IDatabaseConnectionProvider
{
    List<T> GetRecords<T>();
}

Now back in our provider class, create the database connection inside this GetRecords method.

// DatabaseConnectionProvider.cs 
// after DatabaseConnectionProvider(IConfigration)

    public List<T> GetRecords()
    {
        using (var connection = new SqliteConnection(_connectionString))
        {
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = "SELECT * FROM students;";

            var reader = command.ExecuteReader();
            while (reader.Read())
            {
                Console.WriteLine(reader["name"].ToString())
            }
            connection.Close();
            return new List<T>();
        }
    }

The SqliteConnection class is provided to us by that NuGet package that we installed earlier. This gives us the building blocks of database interaction: connection, issuing commands, and reading the results.

This isn’t our final GetRecords implementation, but it will work and you can test that (if you seeded some sample values into your database). When you run it, the method creates Sqlite database connection and issues a SELECT * command to return all the students. We get back a DataReader, also provided by the NuGet package from earlier, that we can use to read the "name" column of each row before disposing the connection.

We’re connected! Take a moment to celebrate.

The models

Now that we can get the data out of the database, we have a bit of grunt work to do in order to be able to work with it. For each of our database tables that creates a record we want to use in our code, we need to create a model in C# using a class.

In this small sample project, we just need two.

// Student.cs
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string School { get; set; } = string.Empty;
}

// Grade.cs
public class Grade
{
    public int Id { get; set; }
    public int Scored { get; set; }
    public int OutOf { get; set; }
    public int StudentId { get; set; }
}

One of these models represents a single row in a table. Right now, they are sad and empty. Okay, maybe not sad, but definitely empty. But, now we have a problem. How do we get a DataReader to spit out these models that we just created?

Once we get into our read loop, we could access all of the columns by name and manually assign them to the model properties. That would work, but we would have to rename our method to GetStudentRecords, since attempting to query for grades would throw an exception when looking for the "name" or the "school" columns.

Well, then we could create a new GetGradeRecords, but then, of course, we would be rewriting all of the logic inside our original method except for where we construct and return a model.

Instead, we need to offload the parsing responsibility from our DatabaseConnectionProvider. Instead of making our provider responsible for reading data, we can make each model responsible for knowing how to construct itself by giving each one Parse method.

// In Student.cs 
    public string School { get; set; } = string.Empty;

    public Student Parse(IDataReader reader)
    {
        Id = reader.ParseInt("id");
        Name = reader.ParseString("name");
        School = reader.ParseString("school");

        return this;
    }
}

// In Grade.cs
    public int StudentId { get; set; }

    public Grade Parse(IDataReader reader)
    {
        Id = reader.ParseInt("id");
        Scored = reader.ParseInt("scored");
        OutOf = reader.ParseInt("out_of");
        StudentId = reader.ParseInt("student_id");

        return this;
    }
}

If you’re typing along, you probably noticed that ParseInt and ParseString are red. In order to clean up the syntax a bit, I added a couple static extensions methods on the DataReader class. You can add those in a new file.

using System.Data;

namespace Students.Repository.SQL;

public static class ReaderExtensions()
{
    public static int ParseInt(this IDataReader reader, string columnName)
    {
        return reader.GetInt32(reader.GetOrdinal(columnName));
    }

    public static string ParseString(this IDataReader reader, string columnName)
    {
        return reader.GetString(reader.GetOrdinal(columnName));
    }
}

Now all we have to do is let the DatabaseConnectionProvider know that our models have this parse method. We will do this with generics, but we are also going to need to define an interface to expose this method generically…

// ISqlDataParser.cs
using System.Data;

namespace Students.Abstractions;

public interface ISqlDataParser<T>
{
    T Parse(IDataReader reader);
}

… and make sure that our models are inheriting it.

// Student.cs
public class Student : ISqlDataParser<Student>

// Grade.cs
public class Grade : ISqlDataParser<Grade>

Now, let’s update our GetRecords declaration to finish making our Parse methods available.

// In DatabaseConnectionProvider
// Replace public List<T> GetRecords()
public List<T> GetRecords() where T : ISqlDataParser<T>, new()

What this says, is that we can only call GetRecords<T> if the type that we insert for T implements the ISqlDataParser interface and has a parameter-less constructor.

Now that we can safely access Parse we can use it in GetRecords<T>.

// In DatabaseConnectionProvider.cs
// In GetRecords<T>
// Replace everything after:  var reader = command.ExecuteReader();

        var returnList = new List<T>();
        using (var reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                returnList.Add(new T().Parse(reader));
            }
        }
        connection.Close();
        return returnList;
    }
}

The query

Right now, we’re in an interesting state. The GetRecords method is able to parse theoretically infinite data types, but it never will. Why? Because we’ve hard-coded a query into the command object that we’re sending to the database.

Similar to what we did with the Parse method, we need to take away the DatabaseConnectionProvider‘s responsibility to choose the query it should run. That means that we’re going to have to move the query up into a parameter, but, we run into a problem there.

If only add a string parameter to the method, but then we don’t have access to the Command object.

So what?

The Command object is where we add SqlParameters. The only option that we would have at this point would be to use string interpolation to add the values in manually. Forcing users of our provider to build queries this way is not only poor etiquette, it is also a potential security vulnerability. Microsoft has already (hopefully), done a lot of work to secure this Command object against SQL injection attacks. No matter the sanitation that we do, adding the parameters to the correct property on the Command object is the smartest way forward.

In order to keep our provider’s methods modular, we need to abstract this into a class that could be used in conjunction with any model that we create. It will wrap up our intended query with placeholders for parameter values, and a dictionary of our parameters. We’ll call it DataCallSettings (naming is hard).

// DataCallSettings.cs
namespace Students.Models;

public class DataCallSettings
{
    public string SqlCommand { get; set; }
    public Dictionary<string, object> Parameters { get; set; } = new();

    public DataCallSettings(string command)
    {
        SqlCommand = command;
    }

    public void AddParameter(string name, object value)
    {
        Parameters[name] = value;
    }

}

This class is now the only parameter to GetRecords<T>. This is a significantly better developer experience than passing in positional parameters. This is a simple implementation, but if we ever decided to extend it to include support for stored procedures, retry logic, transactions or caching, we can do that without breaking our existing codebase.

Take a look at the finished method with all of the changes that we made.

// In DatabaseConnectionProvider.cs

public List<T> GetRecords<T>(DataCallSettings dcs) where T : ISqlDataParser<T>, new()
{
    using (var connection = new SqliteConnection(_connectionString))
    {
        connection.Open();
        var command = connection.CreateCommand();
        command.CommandText = dcs.SqlCommand;

        foreach (var key in dcs.Parameters.Keys)
        {
            command.Parameters.AddWithValue(key, dcs.Parameters[key]);
        }

        var returnList = new List<T>();
        using (var reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                returnList.Add(new T().Parse(reader));
            }
        }
        connection.Close();

        return returnList;
    }
}

As a bonus, I’ll also give you two lines of code that will let us get single records using all the hard work we did earlier.

public T GetRecord<T>(DataCallSettings dcs) where T : ISqlDataParser<T>, new()
=> GetRecords<T>(dcs).FirstOrDefault() ?? new();

The command

Sometimes you will want to issue a query to the database that won’t return anything. So, our database interface isn’t quite complete. We need to build a method that allows us to send database queries without creating an empty model at the end. It will look very similar to our GetRecords method, but we will use the .ExecuteNonQuery() method instead, and we’ll call it Execute. Creative… I know.

// In DatabaseConnectionProvider.cs
// After GetRecord<T>

public int Execute(DataCallSettings dcs)
{
    using (var connection = new SqliteConnection(_connectionString))
    {
        connection.Open();
        var command = connection.CreateCommand();
        command.CommandText = dcs.SqlCommand;

        foreach (var key in dcs.Parameters.Keys)
        {
            command.Parameters.AddWithValue(key, dcs.Parameters[key]);
        }

        var result = command.ExecuteNonQuery();
        connection.Close();
        return result;
    }
}

This is fine, but we’re duplicating some logic between GetRecords and Execute. Let’s lift it into a fancy new BuildCommand method.

// In DatabaseConnectionProvider.cs
// After Execute()

private SqliteCommand BuildCommand(DataCallSettings dcs, SqliteConnection connection)
{
    var command = connection.CreateCommand();
    command.CommandText = dcs.SqlCommand;

    foreach (var key in dcs.Parameters.Keys)
    {
        command.Parameters.AddWithValue(key, dcs.Parameters[key]);
    }

    return command;
}

And refactor both methods to use it.

// In DatabaseConnectionProvider.cs

// In GetRecords<T>
    connection.Open();
    var command = BuildCommand(dcs, connection);

    var returnList = new List<T>();

// In Execute()
    connection.Open();
    var command = BuildCommand(dcs, connection);

    var result = command.ExecuteNonQuery();

Ah, much better. But, we’re not done yet. Let’s add one more method that we will wind up using a lot in practice. When we perform an INSERT, the database will set the identity column, so let’s make sure that we get that back out.

// In DatabaseConnectionProvider.cs
// Below Execute()

public int ExecuteWithIdentity(DataCallSettings dcs)
{
    if (!dcs.SqlCommand.StartsWith("INSERT"))
    {
        throw new InvalidOperationException();
    }

    using (var connection = new SqliteConnection(_connectionString))
    {
        connection.Open();
        dcs.SqlCommand = $"{dcs.SqlCommand} SELECT last_insert_rowid();";
        var command = BuildCommand(dcs, connection);

        var identity = Convert.ToInt32(command.ExecuteScalar());
        connection.Close();
        return identity;
    }
}

And with that… drumroll please.

We’ve done it! We’ve created, by hand, an way to interact with a database that is extensible and easy to use.

The repository

Now that we’re done creating this database interface, I want to quickly walk through how you can use it. One design pattern that I use daily is the repository design pattern. This is a fancy (and admittedly somewhat confusing) name that we can give to a file that wraps up our data transfer logic, keeping our modules less coupled.

Adding a separate layer allows us to ignore the database’s implementation details when we finally want to send this data to a front end somewhere. So, let’s create a StudentsRepository.

// StudentsRepository.cs
using Students.Abstractions;
using Students.Models;

namespace Students.Repository.SQL;

public class StudentsRepository : IStudentsRepository
{

}

And here’s the interface, complete with the methods that we’re going to create.

using Students.Models;

namespace Students.Abstractions;

public interface IStudentsRepository
{
    Student GetStudent(int id);
    List<Student> GetStudents();
    int SaveStudent(Student student);
    bool DeleteStudent(int id);
}

Then, back in our repository, we will inject our DatabaseConnectionProvider. Make sure that it’s registered in your Program.cs file first though. Here’s an article that walks through DI in ASP.NET if you need some help with that.

Finishing the implementation now is as simple as calling the appropriate provider method with a DataCallSettings instance with a SQL query. Instead of walking through step by step, I’ll just include the whole file below, so that you can see how it will look.

// in StudentsRepository.cs

public class StudentsRepository : IStudentsRepository
{
    private readonly IDatabaseConnectionProvider _provider;

    StudentsRepository(IDatabaseConnectionProvider provider)
    {
        _provider = provider;
    }

    public Student GetStudent(int id)
    {
        var dcs = new DataCallSettings("SELECT * FROM students WHERE Id = @Id;");
        dcs.AddParameter("Id", id);
        return _provider.GetRecord<Student>(dcs);
    }

    public List<Student> GetStudents()
    {
        var dcs = new DataCallSettings("SELECT * FROM students;");
        return _provider.GetRecords<Student>(dcs);
    }

    public int SaveStudent(Student student)
        => student.Id == 0 // new student
            ? InsertStudent(student)
            : UpdateStudent(student);

    private int InsertStudent(Student student)
    {
        var dcs = new DataCallSettings("INSERT INTO students (name, school) VALUES (@Name, @School);");
        AddStudentParameters(dcs, student);
        return _provider.ExecuteWithIdentity(dcs);
    }

    private int UpdateStudent(Student student)
    {
        var dcs = new DataCallSettings("UPDATE students SET name = @Name, school = @School WHERE id = @Id;");
        dcs.AddParameter("Id", student.Id);
        AddStudentParameters(dcs, student);
        return _provider.Execute(dcs) > 0
            ? student.Id
            : 0;
    }

    public bool DeleteStudent(int id)
    {
        var dcs = new DataCallSettings("DELETE FROM students WHERE id = @Id");
        dcs.AddParameter("Id", id);
        return _provider.Execute(dcs) > 0;
    }

    private void AddStudentParameters(DataCallSettings dcs, Student student)
    {
        if (student.IsNew) { dcs.AddParameter("Id", student.Id); }
        dcs.AddParameter("Name", student.Name);
        dcs.AddParameter("School", student.School);
    }
}

The conclusion

Okay, I’ll admit that this more code than you probably need if you were using Entity Framework. But… it’s not that much more. I find myself far more comfortable working in an application that has this level of transparency and flexibility than one that does mapping magic. To me, this is an easy trade-off.

This implementation isn’t clever, it’s not complicated, and it’s easy to work with, even if it takes a little bit of getting used to. Over the past two years I’ve been programming, a significant percentage of bugs I deal with are caused by data being malformed. Maybe this is skill issues, but it has made me slightly paranoid about data handling in apps that I work on.

Using this pattern, it’s being handled completely by me. If there’s a problem, I know that it was something I wrote (read: something I can fix). I created the database, I wrote the SQL queries. Since I am responsible for this system, I want to know how it’s handling my data.

This isn’t to say that ORMs are bad. I might not be very good at working with them (see my reference to skill issues above). But, if you’ve never tried to use ADO.NET, I hope you feel that you have the tools to get started.

How I mapped 600+ GPS audio-guides as a solo dev (and why I finally did it after 8 years)


Building a travel tech app wasn’t just a coding challenge for me (i barely know how to code); it was a way to process grief.

Eight years ago, I wrote down a business canvas for an app that would kill the traditional “guided tour”. I hated rigid schedules, and I hated arriving at a beautiful natural trail only to find a faded tourist sign from 1998. I wanted a pocket guide. But life got in the way, and the idea stayed in a drawer.

Recently, I lost my mother. She was the person who instilled in me a deep curiosity for the history that surrounds us. Grieving is a strange process. For me, it meant finally sitting down, opening my IDE, and turning that 8-year-old idea into reality as a tribute to her.

I built YouGuide: a mobile app that uses background geolocation to trigger audio stories. You put your headphones on, lock your phone, and walk. When you reach a historical monument or a trail, the app tells you its story.

As a solo dev from Argentina, the challenges were huge:

Data Collection: I manually mapped and curated over 600 historical and natural points across 90 cities.

UX/UI: Designing an interface meant to be ignored (the goal is for users to look at the landscape, not the screen).

What’s Next?
I just launched the Beta version (it’s 100% free). My current stack relies on tracking early user events to see where the drop-offs are during the onboarding flow.

Building in public is new to me, but I wanted to share this milestone. If you are building geolocation apps or just love traveling, I’d be incredibly grateful if you could check out YouGuide and give me your most brutal feedback.

Link to the project is on my profile. Thanks for reading!

Installing Terminal & WSL (Windows Subsystem for Linux)

WSL is a layer between Windows & Linux (Generated with ChatGPT)

If you’re a web developer on Windows, Windows Subsystem for Linux (WSL) is essential. This tutorial walks you through understanding what WSL is, why you should use it, and how to install and configure it for development.

Note: This guide is for Windows only. If you’re on macOS, you already have a Unix-based terminal, so you can skip this tutorial and move on to the next one in the series.

Do I really need WSL?

Short answer: Yes, if you’re doing web development on Windows.

Most web development tools, packages, and tutorials assume a Unix-based environment (Linux or macOS). While you can develop on Windows using PowerShell or cmd.exe, you’ll constantly run into compatibility issues with package managers, build tools, and open-source projects that assume a Unix shell. WSL lets you run Linux directly on Windows without the overhead of a virtual machine.

What exactly is WSL?

Windows Subsystem for Linux (WSL) is a compatibility layer that allows you to run a genuine Linux environment directly on Windows. It runs a real Linux kernel in a lightweight virtual machine while remaining tightly integrated with Windows—you get the speed and compatibility of Linux with the convenience of Windows.

WSL is a layer between Windows & Linux (Generated with ChatGPT)

What’s a Linux distribution (distro)?

A Linux distribution is a packaged version of the Linux kernel bundled with specific tools, package managers, and system utilities. Common distributions include Ubuntu, Debian, Fedora, and Alpine. For development, Ubuntu is the most beginner-friendly and widely supported choice.

Why WSL instead of dual-booting Linux?

Dual-booting lets you choose your OS at startup, but you can’t use Windows and Linux simultaneously without rebooting. WSL gives you both environments running at the same time with seamless file access between them. You get the best of both worlds.

Why WSL instead of a virtual machine (VM)?

VMs use more system resources (CPU, RAM, storage) than WSL. WSL is lightweight, boots faster, and integrates better with Windows. If you’re resource-constrained or want a quick development environment, WSL is superior.

Do I lose anything by using WSL?

WSL gives you a Linux shell and command-line tools, but it doesn’t run a full graphical Linux desktop by default. You’ll still use Windows for your GUI applications (VS Code, browsers, etc.) and access Linux through the terminal. This is exactly what most developers want.

💻 Install Windows Terminal (Recommended)

Windows Terminal is Microsoft’s modern terminal application. Instead of using the default Windows PowerShell or cmd.exe, Windows Terminal provides a cleaner, more customizable experience with multiple tabs, themes, and seamless WSL integration.

What’s a Terminal? As explained in FAQ: Operating Systems, Terminals & Shells, a terminal is the application that runs a shell (the command-line interpreter). Windows Terminal is the container app, while Bash/Zsh/PowerShell are the shells inside it. See that FAQ for deeper explanation.

Install from Microsoft Store:

  1. Open the Microsoft Store (search “Microsoft Store” in the Windows Start menu)
  2. Search for “Windows Terminal”
  3. Click “Install”

That’s it! Once installed, Windows Terminal is ready to use. You’ll use it to run the WSL installation command in the next section. After WSL is installed, Windows Terminal will automatically recognize Ubuntu as an available shell profile.

💻 Install WSL

These instructions are for Windows 10 (Build 19041+) or Windows 11.

Step 1: Check Your Windows Version

Press Win + R, type winver, and press Enter. You need:

  • Windows 10: Version 21H2 or later (Build 19041 or higher)
  • Windows 11: Any version

If you need to update, go to Settings → System → About → Windows Update.

Step 2: Enable WSL

Open Windows Terminal as Administrator:

  1. Right-click the Windows Terminal icon (in Start menu or taskbar)
  2. Select “Run as Administrator”
  3. You may see a prompt asking “Do you want to allow this app to make changes?” — click “Yes”

Once Windows Terminal opens with admin privileges, you’ll see a command prompt. Regardless of which shell it displays (PowerShell, cmd.exe, or another), the wsl --install command works in any of them. Simply paste or type:

wsl --install

This single command will:

  1. Enable WSL and the Virtual Machine Platform feature
  2. Download and install Ubuntu (the default distro)
  3. Automatically configure the latest version for you

Note: If you see an error about virtualization, you may need to enable it in your BIOS. Restart your computer, enter BIOS (usually by pressing F2, F10, DEL, or ESC during startup — varies by manufacturer), and look for “Virtualization” or “Intel VT-x” / “AMD-V” and enable it.

Step 3: Restart Your Computer

WSL requires a system restart. Do this now.

Step 4: Complete Ubuntu Setup

After restarting, Ubuntu may launch automatically, or you may need to launch it manually:

If Ubuntu launches automatically: You’ll see a terminal window prompt you to create a username and password. Skip to the next section.

If Ubuntu doesn’t launch automatically: Open Windows Terminal, click the dropdown arrow in the toolbar, and select “Ubuntu” to launch the setup.

Either way, you’ll see a setup prompt:

Installing, this may take a few minutes...
Please create a default UNIX user account. The username does not need to match your Windows user name.
For more information visit: https://aka.ms/wsluserstore
Enter new UNIX username: [type your username]
New password: [type a password]
Retype new password: [confirm password]

Choose a simple username (e.g., dev or your first name). Remember this password — you’ll need it for administrative tasks in WSL.

https://pawper.dev/?v=entry&cat=logs&entry=latest&modal=log&id=guide-password-management-cybersecurity-beginners

Step 5: Verify Installation

In the Ubuntu terminal that’s now open, verify everything works:

wsl --version

You should see output showing WSL version 2.x.

Also verify your Linux distribution:

lsb_release -a

You should see Ubuntu information.

Sources / additional material:

https://docs.microsoft.com/en-us/windows/wsl/install

https://docs.microsoft.com/en-us/windows/wsl/about

https://docs.microsoft.com/en-us/windows/wsl/setup/environment

Step 6: Configure Windows Terminal for Ubuntu

Now that WSL and Ubuntu are installed, you can use Windows Terminal to access your Linux environment. Open Windows Terminal and you’ll see a dropdown arrow in the top toolbar — click it and select “Ubuntu” to launch your WSL bash shell.

Set Ubuntu as Default (Optional): If you want Ubuntu to open automatically when you launch Windows Terminal, go to Settings (Ctrl+,), find “Startup” in the left sidebar, and set “Default profile” to “Ubuntu”.

Configure WSL for Development

Update Your Linux Packages

WSL comes with Ubuntu, but the package lists may be outdated. Run:

sudo apt update
sudo apt upgrade -y

The sudo command runs commands with administrator privileges. You’ll be prompted for your password (the one you created in Step 4).

Install Essential Build Tools

Most web development depends on a C/C++ compiler. Install it:

sudo apt install build-essential -y

This installs gcc, g++, make, and other tools needed to compile native packages.

(Optional) Install Git

If you don’t have Git installed on your Windows machine, install it in WSL:

sudo apt install git -y

Note: You can use either the Windows version or WSL version of Git. I recommend installing it in both places so you have flexibility.

(Optional) Install Node.js (via nvm)

Many web tutorials use Node.js. The easiest way to manage Node versions is with nvm (Node Version Manager):

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

Then reload your shell:

source ~/.bashrc

Install Node.js:

nvm install node

Verify:

node --version
npm --version

Access Your Files

WSL and Windows share a file system, but you need to know the paths:

From WSL, Access Windows Files

Your Windows C: drive is mounted at /mnt/c/ in WSL:

cd /mnt/c/Users/YourUsername/Documents

From Windows, Access WSL Files

Your WSL home directory is located at:

\wsl$Ubuntuhomeusername

Open File Explorer and type this path in the address bar. You can also open it with explorer.exe ~ from the WSL terminal.

Best Practice

Keep your development projects in WSL’s home directory (~/projects/ or similar), not in Windows. This avoids file permission issues and improves performance.

Shared file system (Generated with ChatGPT)

Troubleshooting

WSL Doesn’t Start / “Command Not Found”

Make sure you ran wsl --install in PowerShell as Administrator, not cmd.exe or a regular PowerShell window.

“Virtualization is not enabled”

You need to enable virtualization in your BIOS. Restart your computer, enter BIOS (varies by manufacturer), and look for “Virtualization” or “VT-x” / “AMD-V”.

WSL Is Slow

This might mean you’re accessing files across the Windows/WSL boundary. Keep your projects in WSL’s home directory, not /mnt/c/.

Forgot Your WSL Password

In PowerShell, reset it with:

wsl --user root
passwd username

Then exit and log back in as your regular user.

Sources / additional material:

https://docs.microsoft.com/en-us/windows/wsl/install
https://docs.microsoft.com/en-us/windows/wsl/about
https://docs.microsoft.com/en-us/windows/wsl/setup/environment
https://docs.microsoft.com/en-us/windows/wsl/troubleshoot/common-issues
https://docs.microsoft.com/en-us/windows/wsl/setup/windows-terminal

This article was generated with AI for the purpose of providing practical information. I have reviewed it for accuracy and edited it appropriately.

A Floating Productivity Panel I Built for Android

Introducing Floatkit — a floating productivity panel for Android.

The Problem

Every Android user knows this feeling.

You are in the middle of something — a conversation, a video, a document — and you need to do one small thing. Check a task. Write a thought down. Find a file. Copy a link.

So you switch apps. Then switch back. Then switch again.

Every switch breaks your focus. Every interruption costs you time. And there is no native Android solution that solves this. No floating panel. No always-accessible toolbar. Just endless app switching.

The Solution

Floatkit is a floating productivity sidebar that lives on the edge of your screen and works on top of every app on your phone.

One tap to open. One swipe to hide. No switching. No interruptions. Just instant access to every tool you need — right where you are.

What’s Inside

🔍 Global Search
Search your files, contacts, calendar, installed apps, notes, tasks and the web from a single floating search bar. Everything on your device, instantly findable.

📝 Quick Notes
Capture ideas, thoughts and links instantly without leaving your current app. Your notes are always one tap away.

✅ Smart Task Manager
Add, complete and manage your to-do list on top of any app. Never lose track of what needs to get done.

📋 Clipboard Manager
Manually save important copied text permanently. No more losing that link or number you copied a few minutes ago.

🎤 Voice Memos
Record reminders, ideas or audio notes instantly while using any other app.

📱 App Launcher
Search, find and launch any installed app directly from the floating dock. Pin your favorites for one-tap access.

🎛️ Tools Panel
Adjust brightness, control volume, take screenshots and access the power menu — all without leaving your current app.

📸 Recent Screenshots
View and manage your latest captures directly from the panel.

Privacy First

Everything in Floatkit is stored locally on your device.

No account required. No cloud storage. No data collection. No internet needed for any core feature. Your notes, tasks, recordings and files stay exactly where they belong — with you.

Free to Download

Floatkit is free on Android with a one-time premium unlock for unlimited notes, tasks and clipboard history. No subscription. No recurring charges. Own it forever.

👉 Download Floatkit on Google Play

Questions or feedback: [email protected]