Reading Notes #695

A mix of thoughtful perspectives and practical updates this week. From evolving AI tools and model selection guidance to changes in developer workflows and tooling, there’s plenty to reflect on. Add in insights on streaming and a strong push toward more secure environments, and you get a well-rounded set of reads worth your time.


Suggestion of the week

AI

Programming

Miscellaneous

  • Livestreaming Before It Was Cool (Golnaz) - Curious to learn more about the streaming options from the different platforms to the tools, and the pro and cons of each? This post is for you, and on top of that, you get the Microsoft story.
~frank


Reading Notes #694

A fast-moving mix this week: AI tooling, ARM readiness, Docker sandboxes, and real-world lessons from agents. Practical insights across .NET, DevOps, and local-first workflows.


Suggestion of the week

AI

Programming

DevOps

Podcasts

  • Our Favorite Agent Setups (Agentic DevOps) - Nice discussion that goes through many AI harnesses, agents, models, and what they are playing with right now. OpenClaw, OpenCode, Claude Code, Copilot, and all of it.

  • Michael Perry: AI-assisted Development - Episode 397 (AI DevOps Podcast) - Interesting discussion about AI-assisted Development (or can we say programming?) with a focus on skills and how they could be defined.


Reading Notes #693

I'm always on the lookout for innovative ways to enhance my coding experience, and this week's Reading Notes are filled with exciting discoveries! From cutting-edge UI libraries to secure sandbox environments for AI agents, I've curated a selection of articles that showcase the latest programming trends and technologies. 

Whether you're interested in harnessing the power of Docker sandboxes or exploring the potential of smart glasses integration, there's something on this list for everyone.


Programming

AI

Miscellaneous

~frank

How to Serve a Vision AI Model Locally with vLLM and Reka Edge

Running an AI model as a one-shot script is useful, but it forces you to restart the model every time you need a result. Setting it up as a service lets any application send requests to it continuously, without reloading the model. This guide shows how to serve Reka Edge using vLLM and an open-source plugin, then connect a web app to it for image description and object detection.

Prerequisites

You need a machine with a GPU and either Linux, macOS, or Windows (with WSL). I use UV, a fast Python package and project manager, or pip + venv if you prefer.

Clone the vLLM Reka Plugin

Reka models require a dedicated plugin to run under vLLM, not all models need this extra step, but Reka's architecture requires it. Clone the plugin repository and enter the directory:

git clone https://github.com/reka-ai/vllm-reka
cd vllm-reka

The repository contains the plugin code and a serve.sh script you will use to start the service.

Download the Reka Edge Model

Before starting the service, you need the model weights locally. Install the Hugging Face Hub CLI and use it to pull the reka-edge-2603 model into your project directory:

uv sync
uv pip install huggingface_hub
uvx hf download RekaAI/reka-edge-2603 --local-dir ./models/reka-edge-2603

This is a large model, so make sure you have enough disk space and a stable connection.

Start the Service

Once the model is downloaded, start the vLLM service using the serve.sh script included in the plugin:

uv run bash serve.sh ./models/reka-edge-2603

The script accepts environment variables to configure which model to load and how much GPU memory to allocate. If your GPU cannot fit the model at default settings, open serve.sh and adjust the variables at the top. The repository README lists the available options. The service takes a few seconds to load the model weights, then starts listening for HTTP requests.

As an example with an NVIDIA GeForce RTX 5070, here are the settings I used to run the model:

export GPU_MEM=0.80
export MAX_LEN=4096
export MAX_BATCH_TOKENS=4096
export MAX_IMAGES=2
export MAX_VIDEOS=1
export VIDEO_NUM_FRAMES=4
uv run bash serve.sh ./models/reka-edge-2603

Connect the Media Library App

With the backend running, time to start the Media Library app. Clone the repository, jump into the directory, and run it with Docker:

git clone https://github.com/fboucher/media-library
cd media-library
docker compose up --build -d

Open http://localhost:8080 in your browser, then add a new connection with these settings:

  • Name: local (or any label you want)
  • IP address: your machine's local network IP (e.g. 192.168.x.x)
  • API key: leave blank or enter anything — no key is required for a local connection
  • Model: reka-edge-2603

Click Test to confirm the connection, then save it.


Try It: Image Description and Object Detection

Select an image in the app and choose your local connection, then click Fill with AI. The app sends the image to your vLLM service, and the model returns a natural language description. You can watch the request hit your backend in the terminal where the service is running.

Reka Edge also supports object detection. Type a prompt asking the model to locate a specific feature (ex: "face") and the model returns bounding-box coordinates. The app renders these as red boxes overlaid on the image. This works for any region you can describe in a prompt.



Switch to the Reka Cloud API

If your local GPU is too slow for production use, you can point the app at the Reka APIs instead. Add a new connection in the app and set the base URL to the Reka API endpoint. Get your API key from platform.reka.ai. OpenRouter is another option if you prefer a unified API across providers.

The model name stays the same (reka-edge-2603), so switching between local and cloud is just a matter of selecting a different connection in the app. The cloud API is noticeably faster because Reka's servers are more powerful than a local GPU (at least mine :) ). During development, use the local service to avoid burning credits; switch to the API for speed when you need it.

What You Can Build

The service you just set up accepts any image, or video via HTTP — point a script at a folder and you have a batch pipeline for descriptions, tags, or bounding boxes. Swap the prompt and you change what it extracts. The workflow is the same whether you are running locally or through the API.

References

Reading Notes #692

The tech landscape is constantly evolving, and keeping up with the latest developments can be overwhelming. From AI-powered tools like Ollama and OpenClaw, to new ways of programming with Aspire Docs and Azure CLI, it seems like there's always something new to explore. In this edition of Reading Notes, I'll share some of the interesting things that caught my eye recently, from AI advancements to developer tools and beyond.


Suggestion of the week

AI

Programming

Cloud

Miscellaneous


Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, podcasts and books that catch my interest during the week.

If you have interesting content, share it!

~frank

Adding Keycloak Authentication to an Existing .NET Aspire Application

By the end of this post, you'll have a working login/logout flow backed by Keycloak, running locally via Aspire and deployable via Docker Compose.

If your Aspire app doesn't have authentication yet, this is your fastest path to a real identity provider. This tutorial walks through wiring Keycloak OIDC into an existing .NET Aspire + Blazor Server app: from AppHost registration to login/logout UI, using production code from NoteBookmark, an open-source bookmark manager built with .NET Aspire and Blazor.\

[version en français disponible]

Step 1: Add Aspire.Hosting.Keycloak to AppHost

Aspire provides first-class Keycloak support through the Aspire.Hosting.Keycloak package. Add it to your AppHost project:

For AppHost project

dotnet add package Aspire.Hosting.Keycloak

Run dotnet restore to pull the package.


Step 2: Register Keycloak in AppHost.cs

With the package installed, register Keycloak as a resource in your AppHost. Aspire will spin up a Keycloak container, wire its connection details into dependent projects, and ensure proper startup ordering.


// ...

// Add Keycloak authentication server
var keycloak = builder.AddKeycloak("keycloak", port: 8080)
    .WithDataVolume(); // Persist Keycloak data across container restarts

if (builder.Environment.IsDevelopment())
{
    // ...

    builder.AddProject<NoteBookmark_BlazorApp>("blazor-app")
        // ...
        .WithReference(keycloak)  // <-- reference Keycloak
        .WaitFor(keycloak)  // <-- wait for Keycloak to be ready
        .WithExternalHttpEndpoints()
        .PublishAsDockerComposeService((resource, service) =>
        {
            service.ContainerName = "notebookmark-blazor";
        });
}

Key Changes:

  • AddKeycloak("keycloak", port: 8080): Registers a Keycloak resource listening on port 8080.
  • WithDataVolume(): Persists Keycloak's configuration and realm data across container restarts. Without this, you'd lose your realm setup every time the container stops.
  • .WithReference(keycloak): Injects Keycloak connection settings (base URL, etc.) into the BlazorApp as environment variables.
  • .WaitFor(keycloak): Ensures Keycloak is fully started before launching the Blazor app. This is critical: if your app starts before Keycloak is ready, OIDC discovery will fail.

 

Step 3: Set Up Keycloak for Non-Aspire (aka prod) Deployments

This post focuses on the Aspire dev setup, but for production (Docker Compose, Kubernetes), you need a standalone Keycloak. Here's what that looks like and why.

Aspire can actually help you bridge the gap. AddDockerComposeEnvironment() in AppHost generates a draft Docker Compose file from your Aspire model, a great starting point before customizing for production. Worth checking out if you want a head start.

The final compose files for both Keycloak and the NoteBookmark app are available in the NoteBookmark repo:

A few things worth noting about the setup:

  • Postgres as the backing store: Keycloak uses a dedicated Postgres instance (not the app's database) to persist realm configuration, users, and sessions.
  • KC_HTTP_ENABLED: "true": Allows HTTP traffic internally. In production, Keycloak runs behind a reverse proxy (nginx, Traefik) that handles TLS termination: HTTPS externally, HTTP internally.
  • KC_FEATURES: "token-exchange": Enables the token exchange feature, needed if you want service-to-service auth flows.

Step 4: Configure Keycloak Realm and OIDC Client

This configuration is required in both production and development environments, but only needs to be done once per environment. In dev, thanks to .WithDataVolume(), all Keycloak settings are persisted between run and debug sessions, so you only configure it once and it survives restarts.

Once Keycloak is running, configure it:

  1. Navigate to http://localhost:8080 and log in with your admin credentials.
  2. Create a new realm:
    • Click Create Realm
    • Name: notebookmark (match the realm in your Authority URL below)
  3. Create an OIDC client:
    • Clients → Create Client
    • Client ID: notebookmark
    • Client Protocol: openid-connect
    • Access Type: confidential (generates a client secret)
    • Valid Redirect URIs: http://localhost:5173/* (adjust for your Blazor app's URL)
    • Web Origins: http://localhost:5173
  4. Go to the Credentials tab and copy the Client Secret: you'll need this in your app config.
Keycloak client configuration screen


Step 5: Add OpenID Connect to the Blazor App

Now wire up the authentication pipeline in your Blazor Server app.

Add the NuGet Package


For BlazorApp project

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Update Program.cs

BlazorApp/Program.cs:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

//...

// Add authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    var authority = builder.Configuration["Keycloak:Authority"];
    options.Authority = authority;
    options.ClientId = builder.Configuration["Keycloak:ClientId"];
    options.ClientSecret = builder.Configuration["Keycloak:ClientSecret"];
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;

    // Allow overriding RequireHttpsMetadata via configuration.
    // Relax the requirement when running in a container against HTTP Keycloak.
    var requireHttpsConfigured = builder.Configuration.GetValue<bool?>("Keycloak:RequireHttpsMetadata");
    var isRunningInContainer = string.Equals(
        System.Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"),
        "true",
        StringComparison.OrdinalIgnoreCase);

    if (requireHttpsConfigured.HasValue)
    {
        options.RequireHttpsMetadata = requireHttpsConfigured.Value;
    }
    else
    {
        var defaultRequireHttps = !builder.Environment.IsDevelopment();
        if (isRunningInContainer &&
            !string.IsNullOrEmpty(authority) &&
            authority.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
        {
            defaultRequireHttps = false;
        }
        options.RequireHttpsMetadata = defaultRequireHttps;
    }

    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");

    options.TokenValidationParameters = new()
    {
        NameClaimType = "preferred_username",
        RoleClaimType = "roles"
    };

    // Configure logout to pass id_token_hint to Keycloak
    options.Events = new OpenIdConnectEvents
    {
        OnRedirectToIdentityProviderForSignOut = async context =>
        {
            var idToken = await context.HttpContext.GetTokenAsync("id_token");
            if (!string.IsNullOrEmpty(idToken))
            {
                context.ProtocolMessage.IdTokenHint = idToken;
            }
        }
    };
});

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();

// ... existing Razor Components, FluentUI, etc. ...

var app = builder.Build();
app.MapDefaultEndpoints();

// ... existing middleware ...

// CRITICAL: UseAuthentication BEFORE UseAuthorization
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

// Authentication endpoints
app.MapGet("/authentication/login", async (HttpContext context, string? returnUrl) =>
{
    var authProperties = new AuthenticationProperties { RedirectUri = returnUrl ?? "/" };
    await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
});

app.MapGet("/authentication/logout", async (HttpContext context) =>
{
    var authProperties = new AuthenticationProperties { RedirectUri = "/" };
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
});

app.Run();


Configuration


Create or update appsettings.json in the BlazorApp project:

{
  "Keycloak": {
    "Authority": "http://localhost:8080/realms/notebookmark",
    "ClientId": "notebookmark",
    "ClientSecret": "your-client-secret-from-keycloak",
    "RequireHttpsMetadata": false
  }
}

For your Prod, Docker Compose deployments, use environment variables in your docker-compose.yaml:

environment:
  Keycloak__Authority: ${KEYCLOAK_AUTHORITY}
  Keycloak__ClientId: ${KEYCLOAK_CLIENT_ID}
  Keycloak__ClientSecret: ${KEYCLOAK_CLIENT_SECRET}
In the development enviroment, Aspire's .WithReference(keycloak) automatically injects environment variables like services__keycloak__http__0 for the Keycloak base URL. You can read this in your config or manually set the Authority URL as shown above.

 

Handling HTTP vs HTTPS: The RequireHttpsMetadata Gotcha

By default, the OpenID Connect middleware requires HTTPS for metadata discovery (RequireHttpsMetadata = true). This is a security best practice for production, but it causes problems in local/container dev environments where Keycloak runs on HTTP.

The code above implements a smart fallback:

  1. Check explicit configuration first: If Keycloak:RequireHttpsMetadata is set in config, use that value.
  2. Detect container environment: If running in a container (DOTNET_RUNNING_IN_CONTAINER=true) and the Authority URL is HTTP, disable the HTTPS requirement.
  3. Default to HTTPS in production: Outside of Development mode, default to requiring HTTPS.

This ensures:

  • Local dev works seamlessly with HTTP Keycloak
  • Container-to-container communication works (HTTP internally)
  • Production enforces HTTPS (assuming you've configured it properly)

Note: In production, run Keycloak behind a reverse proxy (nginx, Traefik, etc.) that handles TLS termination. Your app sees https://yourdomain.com, Keycloak internally runs on HTTP.

That's the server-side setup done. Now let's build the Blazor UI pieces that make auth visible to users.


Step 6: Blazor UI: Login, Logout, and Route Protection

With the backend authentication pipeline configured, it's time to build the UI components that let users actually log in, log out, and interact with protected content. We'll create three key pieces: the login/logout pages, a login display component, and routing configuration that enforces authorization.

Keycloak login screen

The Login and Logout Razor Pages

First, we need pages to trigger authentication flows. These aren't typical Blazor pages with markup—they're redirect triggers that hand off control to Keycloak.

Login.razor

Create Components/Pages/Login.razor:

@page "/login"
@attribute [AllowAnonymous]
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
@inject NavigationManager Navigation
@inject IHttpContextAccessor HttpContextAccessor

@code {
    protected override async Task OnInitializedAsync()
    {
        var uri = new Uri(Navigation.Uri);
        var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
        var returnUrl = query["returnUrl"] ?? "/";

        var httpContext = HttpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            var authProperties = new AuthenticationProperties
            {
                RedirectUri = returnUrl
            };
            await httpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
        }
    }
}

What's happening here?

  • No markup: This page doesn't render anything. Its job is to initiate the OpenID Connect authentication challenge, which redirects the browser to Keycloak.
  • ChallengeAsync: This triggers the OIDC middleware to redirect the user to Keycloak's login page.
  • Return URL: We capture the returnUrl query parameter so users land back where they started after logging in.
  • [AllowAnonymous]: Critical! Without this, the page would require authentication to access, creating a redirect loop.

Logout.razor

Create Components/Pages/Logout.razor:

@page "/logout"
@attribute [AllowAnonymous]
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
@inject IHttpContextAccessor HttpContextAccessor

@code {
    protected override async Task OnInitializedAsync()
    {
        var httpContext = HttpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            var properties = new AuthenticationProperties
            {
                RedirectUri = "/"
            };
            await httpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, properties);
            await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}


Why sign out of TWO schemes?


This is where many implementations fail. OpenID Connect uses a dual authentication scheme:

  1. OpenIdConnect scheme: Handles the protocol dance with Keycloak (redirects, token exchange, logout).
  2. Cookie scheme: Manages the local session in your Blazor app.

When logging out, you must sign out of both, in this order:

  1. OIDC first: This redirects to Keycloak's logout endpoint, ending the SSO session.
  2. Cookie second: This clears the local authentication cookie.

Signing out of only the cookie leaves the Keycloak session active—users can click "Login" and get back in without re-entering credentials. Signing out only from OIDC leaves the local cookie intact, so the app still thinks they're logged in.

The RedirectUri in the authentication properties controls where users land after the Keycloak logout completes. We send them to the home page.

 

Step 7: The LoginDisplay Component

Now we need a UI element to show login state and provide login/logout actions. This typically lives in your app's header or navigation bar.

Note: NoteBookmark uses FluentUI Blazor (the <Fluent...> components), it's not a requirement, but it definitely looks great ;)

Create Components/Layout/LoginDisplay.razor:

@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        <FluentStack Orientation="Orientation.Horizontal" HorizontalGap="8" 
                     HorizontalAlignment="HorizontalAlignment.Right" 
                     VerticalAlignment="VerticalAlignment.Center">
            <span>Hello, @context.User.Identity?.Name</span>
            <FluentButton Appearance="Appearance.Lightweight" OnClick="Logout" 
                          IconStart="@(new Icons.Regular.Size16.ArrowExit())">
                Logout
            </FluentButton>
        </FluentStack>
    </Authorized>
    <NotAuthorized>
        <FluentButton Appearance="Appearance.Accent" OnClick="Login" 
                      IconStart="@(new Icons.Regular.Size16.Person())">
            Login
        </FluentButton>
    </NotAuthorized>
</AuthorizeView>

@code {
    private void Login()
    {
        var returnUrl = Navigation.ToBaseRelativePath(Navigation.Uri);
        if (string.IsNullOrEmpty(returnUrl)) returnUrl = "/";
        Navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(returnUrl)}", forceLoad: false);
    }

    private void Logout()
    {
        Navigation.NavigateTo("/logout", forceLoad: false);
    }
}

Key implementation details:

  • @rendermode InteractiveServer: This is essential. <AuthorizeView> needs to access AuthenticationStateProvider, which requires an interactive render mode. Without this, the component renders as static HTML and won't respond to auth state changes.
  • <AuthorizeView>: This component automatically shows/hides content based on authentication state. The context parameter provides access to the User claims principal.
  • Return URL on login: We pass the current page URL so users return to where they were after authenticating.
  • forceLoad: false: We use in-app navigation. The Login.razor and Logout.razor pages will handle the actual HTTP redirects.

Add this component to your MainLayout.razor or header component:<LoginDisplay />

visual of the LoginDisplay

Step 8: Protecting Routes and Pages

With login/logout working, you need to enforce authorization rules. Blazor provides two mechanisms: page-level protection with [Authorize] and inline content protection with <AuthorizeView>.

Updating Routes.razor

First, modify Components/Routes.razor to support authorization-aware routing:

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization

<FluentDesignTheme StorageName="theme" @rendermode="@InteractiveServer" />

<CascadingAuthenticationState>
    <Router AppAssembly="typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
                <NotAuthorized>
                    @if (context.User.Identity?.IsAuthenticated != true)
                    {
                        <FluentStack Orientation="Orientation.Vertical" VerticalGap="20" 
                                     HorizontalAlignment="HorizontalAlignment.Center" 
                                     Style="margin-top: 100px;">
                            <FluentIcon Value="@(new Icons.Regular.Size48.LockClosed())" Color="Color.Accent" />
                            <h2>Authentication Required</h2>
                            <p>You need to be logged in to access this page.</p>
                            <FluentButton Appearance="Appearance.Accent" 
                                OnClick="@(() => NavigationManager.NavigateTo(
                                    "/login?returnUrl=" + Uri.EscapeDataString(
                                        NavigationManager.ToBaseRelativePath(NavigationManager.Uri)), 
                                    forceLoad: false))">
                                Login
                            </FluentButton>
                        </FluentStack>
                    }
                    else
                    {
                        <FluentStack Orientation="Orientation.Vertical" VerticalGap="20" 
                                     HorizontalAlignment="HorizontalAlignment.Center" 
                                     Style="margin-top: 100px;">
                            <FluentIcon Value="@(new Icons.Regular.Size48.ShieldError())" Color="Color.Error" />
                            <h2>Access Denied</h2>
                            <p>You don't have permission to access this page.</p>
                            <FluentButton Appearance="Appearance.Accent" 
                                OnClick="@(() => NavigationManager.NavigateTo("/", forceLoad: false))">
                                Go to Home
                            </FluentButton>
                        </FluentStack>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="routeData" Selector="h1" />
        </Found>
    </Router>
</CascadingAuthenticationState>

@code {
    [Inject] private NavigationManager NavigationManager { get; set; } = default!;
}


What changed?

  1. <CascadingAuthenticationState>: This wraps the entire router and makes authentication state available to all child components. Without it, <AuthorizeView> and [Authorize] attributes won't work.

  2. <AuthorizeRouteView>: Replaces the standard RouteView. This component checks the [Authorize] attribute on routed pages and enforces authorization rules.

  3. <NotAuthorized> with two states: This is subtle but important. The <NotAuthorized> content renders when authorization fails, but there are two scenarios:

    • Not authenticated (context.User.Identity?.IsAuthenticated != true): The user isn't logged in. Show a "Login" button.
    • Authenticated but not authorized (else): The user is logged in but lacks permission (e.g., wrong role). Show an "Access Denied" message.

Protecting Pages with [Authorize]

To require authentication for an entire page, add the [Authorize] attribute:

@page "/posts"
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization

<PageTitle>My Posts</PageTitle>

<h1>My Posts</h1>

<!-- Your protected content here -->

Now, unauthenticated users who navigate to /posts will see the "Authentication Required" message from Routes.razor, not the page content.

Note: [Authorize] also supports roles and policies (e.g. [Authorize(Roles = "Admin")]) for more granular access control, that's a topic for a future post.


Testing it out:

  1. Run your Aspire app host: dotnet run --project NoteBookmark.AppHost
  2. Navigate to your Blazor app in the browser.
  3. Click "Login"—you should redirect to Keycloak, authenticate, and return.
  4. You'll see "Hello, [your name]" in the header.
  5. Navigate to a page marked [Authorize] without logging in—you'll see the auth required message.
  6. Click "Logout"—you'll sign out of both the app and Keycloak.

Your Blazor app now has full OpenID Connect authentication with Keycloak, with a clean separation between the auth mechanics (Login/Logout pages), UI (LoginDisplay), and enforcement (Routes.razor + [Authorize]).


Conclusion

You've now integrated Keycloak authentication into your .NET Aspire application. The key pieces:

  1. Aspire orchestration: AddKeycloak(), .WithReference(), and .WaitFor() handle container lifecycle and configuration injection.
  2. OIDC pipeline: The standard ASP.NET Core authentication middleware, configured for Keycloak's OIDC endpoints.
  3. HTTP flexibility: Logic to handle HTTP Keycloak in dev while enforcing HTTPS in production.
  4. Persistent data: WithDataVolume() ensures your Keycloak realm config survives restarts.

This pattern scales beyond Keycloak, Aspire's resource model works the same way for databases, message queues, and other services. Once you've mastered .WithReference() and .WaitFor(), you can compose complex distributed systems with confidence.

The full working implementation is available in the NoteBookmark repository, including the AppHost configuration, Blazor components, and Docker Compose files referenced throughout this post.


Useful links


Reading Notes #691

This week’s notes bounce between terminals, copilots, and the shifting shape of AI tools in our daily work. From real-world experiments in large .NET projects to small quality-of-life improvements that just make coding smoother, there’s a lot to chew on. A few links stood out more than expected and might change how you approach your setup or your workflow.

All ready the end of the month!

Open Source

AI

Miscellaneous

~frank


Reading Notes #690

AI keeps changing how we build, think, and even feel about software. This batch of posts & episodes mixes practical agent skills, vibe coding, and faster shipping with a bit of reflection on the old internet and why it still sticks with us.


AI


Podcasts

  • Your Images are Out of Date (probably) - The Silent Rebuilds problem (DevOps and Docker Talk: Cloud Native Interviews and Tooling) - Very interesting episode. I guess I never realized how true it is that as soon as you download your image, they are outdated. This episode talks about the concept of silent rebuilds and tools to help us solve that issue.

  • 503: Welcome to Tiny Tool Town (Merge Conflict) - With a name like Tiny Tool Town, my head always goes to Looney Tunes. No idea why, but this episode is not about that. It's about the collection of open source tools named: Tiny Tool Town, and they also talk about different models in GitHub Copilot.

  • Building Software using Squad with Brady Gaster (.NET Rocks!) - Turn your Coplot to 11 with Squad. Carl and Richard talk to Brady Gaster about Squad, a tool for creating an AI development team using GitHub Copilot.

  • Daniel Ward: AI Agents - Episode 393 (Azure & DevOps Podcast) - In this episode, they talk about the different AI tools used by developers and DevOps people, and the trends.

  • Everything Is a Graph (Even Your Dad Jokes) with Roi Lipman (Screaming in the Cloud) - Nice episode about different database and most obviously about graph databases. Very interesting to learn more about all that explosion of database types.


Miscellaneous


Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, podcasts and books that catch my interest during the week.

If you have interesting content, share it!

~frank


Private Vision AI: Run Reka Edge Entirely on Your Machine

Reka just released Reka Edge, a compact but powerful vision-language model that runs entirely on your own machine. No API keys, no cloud, no data leaving your computer. I work at Reka and putting together this tutorial was genuinely fun; I hope you enjoy running it as much as I did.

[Originally published at dev.to/reka]

In three steps, you'll go from zero to asking an AI what's in any image or video.

What You'll Need

  • A machine with enough RAM to run a 7B parameter model (~16 GB recommended)
  • Git
  • uv, a fast Python package manager. Install it with:
    curl -LsSf https://astral.sh/uv/install.sh | sh
    
    This works on macOS, Linux, and Windows (WSL). If you're on Windows without WSL, grab the Windows installer instead.

Step 1: Get the Model and Inference Code

Clone the Reka Edge repository from Hugging Face. This includes both the model weights and the inference code:

git clone https://huggingface.co/RekaAI/reka-edge-2603
cd reka-edge-2603

Step 2: Fetch the Large Files

Hugging Face stores large files (model weights and images) using Git LFS. After cloning, these files exist on disk but contain only small pointer files, not the actual content.

First, make sure Git LFS is installed. The command varies by platform:

# macOS
brew install git-lfs

# Linux / WSL (Ubuntu/Debian)
sudo apt install git-lfs

Then initialize it:

git lfs install

Then pull all large files, including model weights and media samples:

git lfs pull

Grab a coffee while it downloads, the model weights are several GB.


Step 3: Ask the Model About an Image or Video

To analyze an image, use the sample included in the media/ folder:

uv run example.py \
  --image ./media/hamburger.jpg \
  --prompt "What is in this image?"
the prompt and the burger image

Or pass a video with --video:

uv run example.py \
  --video ./media/many_penguins.mp4 \
  --prompt "What is in this?"

The model will load, process your input, and print a description, all locally, all private.

Try different prompts to unlock more:

  • "Describe this scene in detail."
  • "What text is visible in this image?"
  • "Is there anything unusual or unexpected here?"

What's Actually Happening? 

You don't need this to use the model, but if you're anything like me and can't help wondering what's going on under the hood, here's the magic behind example.py:

1. It picks the best hardware available. The script checks whether your machine has a GPU (CUDA for Nvidia, Metal for Apple Silicon) and uses it automatically. If neither is available, it falls back to the CPU. This affects speed, not quality.

if torch.cuda.is_available():
    device = torch.device("cuda")
elif mps_ok:
    device = torch.device("mps")
else:
    device = torch.device("cpu")

2. It loads the model into memory. The 7 billion parameter model is read from the folder you cloned. This is the "weights": billions of numbers that encode everything the model has learned. Loading takes ~30 seconds depending on your hardware.

processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(args.model, ...).eval()

3. It packages your input into a structured message. Your image (or video) and your text prompt are wrapped together into a conversation-style format, the same way a chat message works, except one part is visual instead of text.

messages = [{
    "role": "user",
    "content": [
        {"type": "image", "image": args.image},
        {"type": "text", "text": args.prompt},
    ],
}]

4. It converts everything into numbers. The processor translates your image into a grid of numerical patches and your prompt into tokens (small chunks of text, each mapped to a number). The model only understands numbers, so this step bridges the gap.

inputs = processor.apply_chat_template(
    messages, tokenize=True, return_tensors="pt", return_dict=True
)

5. The model generates a response, token by token. Starting from your input, the model predicts the most likely next word, then the next, up to 256 tokens. It stops when it hits a natural end-of-response marker.

output_ids = model.generate(**inputs, max_new_tokens=256, do_sample=False)

6. It converts the numbers back into text and prints it. The token IDs are decoded back into human-readable words and printed to your terminal. No internet involved at any point.

output_text = processor.tokenizer.decode(new_tokens, skip_special_tokens=True)
print(output_text)


Here the video

If you prefer watching and reading, here is the video version:

 

That's Pretty Cool, Right?

A single script. No API key. No cloud. You just ran a 7 billion parameter vision-language model entirely on your own machine, and it works whether you're on a Mac, Linux, or Windows with WSL, which is what I was using when I wrote this.

This works great as a one-off script: drop in a file, ask a question, get an answer. But what if you wanted to build something on top of it? A web app, a tool that watches a folder, or anything that needs to talk to the model repeatedly?

That's exactly what the next post is about. I'll show you how to wrap Edge as a local API, so instead of running a script, you have a service running on your machine that any app can plug into. Same model, same privacy, but now it's a proper building block.


~frank 

Reading Notes #689

Another week, another batch of interesting reads. This edition covers AI video experiments, extending coding agents with .NET skills, open source contributions, and a few podcast episodes worth adding to your queue.


AI

Programming

Open Source

Podcasts

Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, podcasts and books that catch my interest during the week.

If you have interesting content, share it!

~frank


Reading Notes #688

I'm always on the lookout for innovative ideas to streamline my development workflow. This week, I stumbled upon some fascinating reads that caught my eye, among them, an article about building an AI-powered pull request agent using GitHub Copilot SDK, and another demonstrating the secure use of OpenClaw in Docker sandboxes.


AI

Programming

~frank


Reading Notes #687

Welcome to this new Reading Notes post, a collection of interesting articles and resources I've been absorbing lately! This week's roundup dives into a variety of topics, from practical storage solutions and leveraging AI for code upgrades to exploring the intersection of AI and business value. Get ready for a diverse mix of tech insights and management reflections.


Programming

AI

Books



The Making of a Manager: What to Do When Everyone Looks to You
(Julie Zhuo) - Most management books are written by advanced managers, people with a lot of experience who already have the "manager" mindset well established in their heads. This book feels different, more accessible, closer to a conversational tone. In this book, Julie shares her stories of becoming a manager and the advice she learned along the way. I think it's a good book to get started on this topic, especially if you are new to that position or thinking about it, to understand and be better equipped for the new challenges coming your way.


Miscellaneous

~frank



Reading Notes #686

This week's Reading Notes is packed with AI insights, open-source discoveries, programming tips, and podcast episodes that will leave you eager to dive in. From Ralph Wiggum's coding secrets to the dangers of one-shot glamour, we've got it all covered. So grab your favourite beverage, settle in, and get ready to level up your tech game!

AI


Open Source


Programming


Podcast

~frank


Reading Notes #685

This week's collection of interesting articles, blog posts, and insights from the world of technology, programming, and AI. From the latest developments in Claude code and AI models for coding to discussions on the security of AI assistants and the future of the craft of programming, there's something for everyone in this edition of Reading Notes. 

Enjoy!

snow in a forest with the shadow of the trees
zebra snow

AI

Programming

  • Is the craft dead? (Scott Hanselman) - Good question! What do you think? Is it still there? I'm personally sure it still is.

Miscellaneous

~frank

Reading Notes #684

Balancing cloud innovation with AI practicality, this week’s notes blend Azure updates, .NET’s AI roadmap, and clever Python hacks. A sharp reminder on burnout prevention anchors the mix, while creative teams and DevOps culture inspire fresh perspectives. From Docker model runners to Git worktrees, every corner here offers actionable insights or a spark of curiosity, no clichés, just tools and truths for developers navigating the stormy seas of tech.


Suggestion of the week

Cloud

AI

Podcasts

Miscellaneous

~frank

Reading Notes #683

A lot of good stuff crossed my radar this week. From Aspire’s continued evolution and local AI workflows with Ollama, to smarter, more contextual help in GitHub Copilot, the theme is clear: better tools, used more intentionally. I also bookmarked a few thoughtful pieces on leadership and communication that are worth slowing down for. Plenty here to explore, whether you’re deep in code or thinking about how teams actually work.

Meetup MsDevMtl

Programming

AI

Open Source

  • The end of the curl bug-bounty (Daniel Stenberg) - I didn't know about this effort, and it's sad to learn about it too now, of course, but I'm glad those programs exist.

Miscellaneous

  • Why I Still Write Code as an Engineering Manager (James Sturtevant) - There is still hope, everyone! But more seriously, an inspiring post that managers should read.

  • The Art of the Oner (Golnaz) - Another great post from Golnaz talks about how to help the message to land. How and why one takes are helping when presenting and the effort it represents.

Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, and books that catch my interest during the week.

If you have interesting content, share it!

~frank