wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Signing your git commits

Hero image for Signing your git commits

Code pedigree is more important than ever with the ever increasing rate of supply chain attacks. Setting up code signing are a few steps, recorded here for reference.

Choice of key formats

While you could pick an ssh or x509 key, I shall stick to gpg and the Ed25519 algorythm.

Act 1 - get macOS ready

When you followed earlier advice you are good to go. If not, install Homebrew and dependencies

brew install gh gpg pinentry-mac

# setup pinentry
mkdir -p ~/.gnupg
chmod 700 ~/.gnupg
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent

Act 2 - create the key

This creates a signing key that lasts for 2 years. Make sure to get the eMail right. After creation we extract the key id.

gpg --batch --quick-generate-key "John Doe <john.doe@example.com>" ed25519 sign
TARGET_EMAIL="john.doe@example.com"
GPG_KEY_ID=$(gpg --list-secret-keys --with-colons "$TARGET_EMAIL" 2>/dev/null \
             | awk -F: '$1=="sec" {print $5; exit}')
echo Your key id is $GPG_KEY_ID

You also can do that manually running gpg --list-secret-keys --keyid-format=long and look for the string after the / in the line starting with sec.

Act 3 - configure your local git

Make sure to get the eMail right

git config --global user.name "John Doe"
git config --global user.email "$TARGET_EMAIL"
git config --global user.signingkey "$GPG_KEY_ID"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git config --global pull.rebase true

Act 4 - Let github know

Display the public key:

gpg --armor --export $GPG_KEY_ID

Go to your Github key settings, click on "New GPG key" and paste it

Act 5 - overwrite for individual repos (optional)

git config user.name "John Doe"
git config user.email "$TARGET_EMAIL"
git config user.signingkey "$GPG_KEY_ID"
git config commit.gpgsign true
git config tag.gpgsign true
git config pull.rebase true

For adventurous souls there is a setup script available to automate this. Use at your own risk.

As usual YMMV


Posted by on 06 August 2026 | Comments (0) | categories: Development Github macOS

welcome http query

Hero image for welcome http query

In 2026, after a 16 years pause, HTTP gained a new keyword QUERY. Much has been said and said about the GET with a body. But why did it take so long?

Agreement not implementation

I'll share a secret: QUERY has been supported by HTTP for a long time. So has HTTP UNIVERSE or HTTP TANGO The protocol doesn't care for the keyword. Your client an server care. HTTP GET can't contain a body - nonsense. Of course it can. And you can expect, unless you control both sides, that it will fail in one way or another.

So the IETF standards are the etiquette of interaction, not the laws. They are the gentlemen agreement stating "If we both follow the RFC, we can have the reasonable expectation that this exchange of information could work".

Hammering out agreements that don't break previous ones it time consuming and tedious, especially in venerable, foundational standards like http. just see how time tested the standards surrounding http are:

Standards

Method Standardized Document
GET 1991 HTTP/0.9
GET, HEAD, POST 1996 RFC 1945
OPTIONS, PUT, DELETE, TRACE, CONNECT 1997 RFC 2068
refined 1999 (HTTP/1.1) RFC 2616
PATCH 2010 RFC 5789
Summarized HTTP semantics 2022 RFC 9110
QUERY 2026 RFC 10008

Extensions

Extension Since Document Methods added
WebDAV (core) 1999 RFC 2518 PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK
revised 2007 RFC 4918
DeltaV (versioning) 2002 RFC 3253 CHECKOUT, CHECKIN, VERSION-CONTROL, REPORT, MERGE, …
Ordered Collections 2003 RFC 3648 ORDERPATCH
ACL (Access Control List) 2004 RFC 3744 ACL
CalDAV (calendaring) 2007 RFC 4791 MKCALENDAR
SEARCH 2008 RFC 5323 SEARCH
BIND (bindings) 2010 RFC 5842 BIND, UNBIND, REBIND
CardDAV (contacts) 2011 RFC 6352 (reuses WebDAV methods)

The WebDAV SEARCH method (2008) was the early inspiration for QUERY — drafts even used the name SEARCH before the working group settled on QUERY to avoid the WebDAV baggage and map cleanly onto the URI query component.

HTTP anything

Don't believe HTTP TANGO would work? Just clone my little fun project, run it and open the developer tools to watch the network.
Works, since it controls both ends.

Fun with HTTP methods

Tooling needs to catch up

Now QUERY is a standard, tooling needs to catch up. Enums need extension and adjacent standards updated. E.g OpenAPI 3.x has no provision for new HTTP syntax, while Bruno and curl do just fine.

As usual YMMV


Posted by on 17 July 2026 | Comments (0) | categories: HTTP(S) OpenAPI WebDevelopment

sharing vs routing

Hero image for sharing vs routing

Common to all collaboration platforms is the ability to define what items (files, emails, chats, videos) get shared with whom and how (read-only, read-write, comment). What is missing is sharing the intend or purpose. There is what, who, how but not why.

Formal workflow systems on the other hand, while rigid in the way they operate, thrive on intend: "for review", "for approval", "for risk analysis" and so on.

Finding the middle ground

Capturing the purpose or intend allows to uncover actual (soft) workflows. The challenge to solve is to make it worthwhile to the regular users to declare their intend (which by itself is hilarious: how to declare the intend "cover-my-backside" in office compatible lingo?)

A few ideas how to implement this:

  • make it easy, the dialogue defining the share includes a field for intend. Most sharing dialogues allow notifications, so it is half way there
  • adjust the language in the UX: add a "route to" capability
  • integrate it into a task management system (I'd favour GTD)
  • add a fancy dashboard to visualize flows
  • find promoters who are keen to try that way of work
  • use machine learning to predict the routing (been there, done that)

Is this a good idea, what do you think?


Posted by on 01 July 2026 | Comments (0) | categories: Software Workflow

My AI coding workflow

Hero image for My AI coding workflow

There are infinite opinions available how to use AI in software development. I haven't formed one, so I'm documenting what, so far has worked for me. I distinguish between the ideation phase, which's output is a set of specs, issues, tickets, whatever you call it and the implementation phase, which's output is working code.

The two phases run in sequence (doh), but not exclusive. E.g. while feature one and two run through implementation, feature three and four csn be in ideation. This post is about the implementation phase.

Chinese whispers (AI edition)

Based on the ideation phase I start with a set of github issues. They tend to be more than single sentence descriptions of an desired outcome. I launch multiple instances of Claude using claude -w, to enable the git worktree support. The exact number depends on your willingness to endure frequent context switching, I'm old so I stick to 2-4.

In each instance I follow the same prompt: "Plan and implement issue #42, share your assumptions, ask clarifying questions, outline options and seek approvals"

AI goes through a round of questions and challenges until we reach consensus and it implements the task at hand. Once done it creates a pull request. The creation of the pull request triggers code reviews. Currently that's GitHub Copilot review and CodeRabbit AI (disclaimer: the link is a referral link). Both agents review the code, come to conclusions and share them in conversations in the pull request. What's interesting is that they both highlight different issues with limited overlap. I'm particularly fond of CodeRabbit's "nitpick" level of feedback.

Once their review is completed, I let the PR merge, most of the time more that one from the various work trees. To close the feedback loop I then prompt: "Visit all pull requests [timeframe] and look for unresolved conversations. Assess each claim for merit and decide if it is still valid. When no longer valid, post your justification as comment. When still valid, create a new issue with detailed explanation and options, so any developer can understand and act on it, then add a comment linking to the issue. In both cases, mark the conversation as resolved."

This approach strikes a balance between "let the agent do its thing" and "keep the development transparent". If your source of truth for development tasks isn't GitHub issues, you need to adjust my approach.

Coding Workflow

All of this gets supported by CLAUDE.md, MCP, Skills, Tools etc, but that's another story for another time.

As usual YMMV


Posted by on 08 May 2026 | Comments (0) | categories: AI Development

Cumulative data modeling (Part 2)

Hero image for Cumulative data modeling (Part 2)

Continuing from Part 1: What if there is an alternative to a normalized RDBMS storage approach, that better fits the process flow?

The routing slip data model

Instead of looking at the physical entities (Suppliers, Ingredients, Cakes, Mixes, Slices etc.), look at the process steps and treat the process steps as our physical entities that store participating data.

Cheesecake factory

The main difference to normalization is to treat data as process local. 1kg flour in the mixing state is a different datapoint from 1kg four in storage silo 3. Routing slips provide the transition. Key difference: Routing slips are accumulative. So the routing slip in the package process contains all the routing slip data of previous process steps. From a database perspective that is a deliberate data duplication, raising blood pressure for data architects.


Read more

Posted by on 04 May 2026 | Comments (0) | categories: CouchDB Development NoSQL SQL

Standards are Standards until they are not

Hero image for Standards are Standards until they are not

In my junior developer days, literally last century, a senior developer enlightened me: "An open standard is only complete once its wording allows all participants to faithfully implement it to be absolutely incompatible to each other"

Ambiguity is king

Our modern software is build on a few standards which made progress at an unprecedented scale possible:

There are plenty more. Each of them have rock solid parts (nobody questions a <h1>) and parts of, let's call them disagreements. I'll pick on JWT as an example:

  • agreed upon are the three parts: header, payload & signature
  • agreed upon ia that the payload is JSON

The actual content of the payload is already subject to interpretation.

  • Keycloak stands out for its flexibility and steep learning curve
  • Refresh tokens are handled quite differently:
  • Keycloak issues refresh tokens as JWT, others as opaque strings
  • Okta doesn't issue refresh tokens for PKCE
  • EntraID returns the Application ID in the aud field instead of an array of target servers, the iss parameter does't match the .well-known info
  • PING Identity sends scopes not as space separated list, but as JSON array

And all implement "the standard"


Posted by on 13 April 2026 | Comments (0) | categories: Development Identity Management JWT WebDevelopment

AI generated blog article hero graphics

Hero image for AI generated blog article hero graphics

I always liked blog entries that feature an eye catching image at the top of an article, except when on a slow connection ¯_(ツ)_/¯. So I was wondering how I could add them to my > 1000 blog articles. Adjusting the mustache template was fast and easy. My first thought was to create one image per category and reuse that. But that would have been too easy.

With a little help of my friends

I wanted the images vary in style and have some level of relation to the article. So I asked Claude for options. It suggested to use the Anthropic API to generate a picture prompt out of the content of each blog article and submit it to DallE3 for image creation.

I liked the idea, so I asked for implementation. Claude generated a JS file that takes the file name as input.

node --env-file=.env generate-cover.mjs ../path/to/blog/article.md

The whole script is available here for your entertainment. To make it more fun: when you click on one of the header images, it will open the prompt used to generate it.

The prompt to create the prompt:

You are an expert at writing prompts for DALL·E 3 image generation.

Read the blog post below and write a single, vivid image prompt (max 200 words) that:
- Captures the post's core theme or mood
- Works as an eye-catching blog cover image
- for the style cycle through the following styles at random:
  - Photorealistic
  - Tasteful illustrative
  - Cyberpunk
  - Vintage
  - Minimalistic
  - Comic book
- Contains NO text, letters, or words in the image
- Avoids clichés (no light bulbs, no handshakes, no generic office scenes)

Reply with ONLY the prompt — no preamble, no explanation.

Enjoy!


Posted by on 08 March 2026 | Comments (0) | categories: AI WebDevelopment

VSCode launch settings for DominoJNX and HCL Notes 14.5

Hero image for VSCode launch settings for DominoJNX and HCL Notes 14.5

I'm not the only one who doesn't like Windows, which is a problem when you depend on Domino Designer. Luckily there is Java and DominoJNX. A decade ago I would use Eclipse on an Intel Mac (and Java8). Much has changed. Java is now 21, the Mac is based on Arm and my primary IDE is VSCode. Here is my setup.

x86 vs. arm

To run a Java app against the Notes 14.5 client there are several steps required:

  • An installed and running Notes 14.5 client (duh)
  • MacOS Rosetta installed
  • a 64Bit x86 JDK (won't work with an ARM JDK)
  • A launch.json file in .vscode of your project

Read more

Posted by on 04 February 2026 | Comments (1) | categories: HCL Notes Java

Cumulative data modeling (Part 1)

Hero image for Cumulative data modeling (Part 1)

The going approach to model data is drawing your ER Diagram and map it to your persistence, be it normalized in a RDBMS or an entry in a NoSQL store.

A slice of cake

Walking through the use case of a cheese cake factory, slightly simplified. All process steps happen in batches, but batch sizes are not synchronized. E.g. dough mixing produces 0.8 - 1.4 times the baking capacity.

Cheesecake factory

The corresponding data model would look like this:

Cheesecake data model

Now imagine a customer wants to know if the slice is made with cheese from "happy cows", which is a trade certification some of the suppliers have. A simple SQL solves it:

SELECT
    s.slice_id,
    ROUND(
        (SUM(CASE WHEN sup.HappyCowCertified = TRUE THEN mi.quantity_used ELSE 0 END) /
         SUM(mi.quantity_used)) * 100,
        2
    ) AS happy_cow_percentage
FROM Slices s
JOIN Baked_Cakes bc ON s.cake_id = bc.cake_id
JOIN Mix_Ingredients mi ON (
    mi.mix_id = bc.dough_mix_id OR
    mi.mix_id = bc.filling_mix_id OR
    mi.mix_id = bc.topping_mix_id
)
JOIN Ingredients i ON mi.ingredient_id = i.ingredient_id
JOIN Ingredient_Batches ib ON mi.batch_id = ib.batch_id
JOIN Suppliers sup ON ib.supplier_id = sup.supplier_id
WHERE s.slice_id = 'someSliceId'
  AND i.name = 'Cream Cheese'
GROUP BY s.slice_id;

Another day another challenge: a batch from a supplier was sub standard and you need to recall all slices made with it. SQL to the rescue again:

SELECT DISTINCT
    s.slice_id,
    s.storage_location
FROM Slices s
JOIN Baked_Cakes bc ON s.cake_id = bc.cake_id
JOIN Mix_Ingredients mi ON (
    mi.mix_id = bc.dough_mix_id OR
    mi.mix_id = bc.filling_mix_id OR
    mi.mix_id = bc.topping_mix_id
)
WHERE mi.batch_id = 'someBadBatchId'
ORDER BY s.slice_id;

As long as as bakery control system has this baked in (pun intended) or you have your residential SQL jockey on speed dial, all is well (keep in mind: the real data model is way more complex), but there might be a better way, slightly unconventional. Continue reading part 2.


Posted by on 23 January 2026 | Comments (0) | categories: CouchDB Development NoSQL SQL

Running your SPA on Cloudflare

Hero image for Running your SPA on Cloudflare

Your application is ready, you want to prepare for the incoming storm of users, so you separate your SPI server from your UI. To host the UI Cloudflare is chosen, while the API server sits in its data centre. There are a few steps to be had, which are simple, just the AI was acting cute, so I write it down.

Cloudflare hosted SPA

Workers & Pages

We shall update our single page application whenever new contenet gets merged into main. All calls to /api/* need to get routed to the API server. These are the steps that wrok. Nota bene: if you want to push from your local machine, the steps are different and a story for another time.

  • In Cloudflare head to Build, Compute & AI, Workers & Pages
  • Don't get distracted by the blue "Create application" button, you want to click above it and select + Add and then Pages (don't select "Worker")
  • Select "Import an existing Git repository" and select your repo (You need the GitHub integration for that).
  • configure your build settings, for a viteJS applicationa, the build command is npm run build and the output directory is dist. Save it and you are good to go.

Next step is to configure "Custom domains", so app.example.com points to your page. Follow the UI, it is almost automagic when Cloudflare is your DNS provider

Redirecting /api to the backend

We want Cloudflare to proxy it, not to 30x redirect. A client wouldn't know that the request gets forwarded. This will allow to harden the API (e.g. drop all request not coming from Cloudflare) later on. Also you don't have to deal with CORS.

Search, with and without AI, suggested solutions revolving around files:

  • create _routes.json
  • code _worker.js
  • use an elaborate wrangler setups.

Those all have one thing in common: they didn't work.

What worked: create a folder functions at the root of your repository. Inside add a file [[path]].js - yes, that's two pairs of square brackets and th letters p a t h. It's the catch all for functions. Inside you add:

export async function onRequest(context) {
  const { request } = context;
  const url = new URL(request.url); // Test endpoint

  if (url.pathname === '/helloworld') {
    return new Response('Workers of the world unite!', {
      headers: { 'Content-Type': 'text/plain' }
    });
  } // Proxy API requests

  if (url.pathname.startsWith('/api/')) {
    url.hostname = 'api.example.com';
    url.port = '4443';
    url.protocol = 'https:';

    return fetch(url, request);
  }

  return context.next();
}

Update

Instead of creating functions/[[path]].js, create functions/api/[[path]].js and simplify the function:

export async function onRequest(context) {
  const { request } = context;
  const url = new URL(request.url);

  url.hostname = 'api.example.com';
  url.port = '4443';
  url.protocol = 'https:';

  return fetch(url, request);
}

Hardening the setup is a story for another time.

As usual YMMV


Posted by on 20 January 2026 | Comments (0) | categories: Cloudflare WebDevelopment