erste Version fertig
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 11 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 11 MiB |
|
After Width: | Height: | Size: 16 MiB |
|
After Width: | Height: | Size: 18 MiB |
|
After Width: | Height: | Size: 33 MiB |
|
After Width: | Height: | Size: 28 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 361 KiB |
@@ -1,161 +0,0 @@
|
||||
#### Freelance
|
||||
|
||||
_08/2021 - present_ ~ **Lead/staff software engineer, freelance**
|
||||
|
||||
I currenly work as a freelance software engineer, in either lead or staff positions. A dedicated page for which can be found [here](/freelance/).
|
||||
|
||||
#### Companion Group Ltd.
|
||||
|
||||
_11/2025 - present_ ~ **Staff engineer for [AA, MMO]**
|
||||
|
||||
_Details will be added in due time._
|
||||
|
||||
_09/2025 - 11/2025_ ~ **Gameplay engineer for Storm Lancers (ProbablyMonsters)**
|
||||
|
||||
Worked on Xbox+DualSense controller input support for PC port of the game, both for EGS (custom logic with RawInput) and Steam (Steam Input API). Additionally contributed to Steamdeck support.
|
||||
|
||||
_05/2025 - 09/2025_ ~ **Senior game backend/SRE engineer for Project Ghost (Fantastic Pixel Castle)**
|
||||
|
||||
- Implementation of back-end functionality (C++)
|
||||
- Containerization of game server and services (Docker, C++, UE5)
|
||||
- Migrating infrastructure to infrastructure-as-code (Terraform, AWS)
|
||||
- Remote log persistence setup for game server and services (C++, UE5)
|
||||
- Contributions to custom build system and local development workflow
|
||||
- Development of various internal tools and APIs
|
||||
|
||||
_01/2025 - 05/2025_ ~ **AI bot engineer, Unreal Engine for [AA]**
|
||||
|
||||
Implementation of AI bots based on behavior trees, with support for game abilities (GAS) by bots. I added support for combat with various weapons and implemented bot spawning based on level settings with support for loadouts. Various game logic was modified to be compatible with bots.
|
||||
|
||||
_10/2024 - present_ ~ **Lead developer, maintainer for [capsa.gg](https://capsa.gg) (open-source)**
|
||||
|
||||
[Capsa](https://capsa.gg) is a non-intrusive open-source logging solution for Unreal Engine. For Capsa, I created the API server and web panel from scratch and contributed to the Unreal Engine plugin. Additionally, I created the documentation website and a video playlist for demo and integration guide.
|
||||
|
||||
_04/2024 - 12/2024_ ~ **Gears of War: E-Day (The Coalition/Microsoft)**
|
||||
|
||||
Supporting Online and Platform Services. _Details will be added in due time._
|
||||
|
||||
_08/2023 - 03/2024_ ~ **Senior game backend engineer for [AAA, Amazon Games, unannounced]**
|
||||
|
||||
- Containerization of game servers and integration with CI/CD (Docker, GitLab)
|
||||
- Development of custom game server orchestration solution (C# .NET 6, SAM, DynamoDB)
|
||||
- Integration of game server orchestration into game server code (C++/Unreal Engine)
|
||||
- Prototype of real-time back-end service to game server communication (MQTT, C++, Unreal Engine)
|
||||
- Integration of Amazon GameLift into custom Online Subsystem (AWS, C++/Unreal Engine)
|
||||
- Development of API server between players and GameLift (Golang, JWT, JWK, Kubernetes, Helm)
|
||||
- Game server infrastructure and deployment automation (Terraform, AWS, GitLab, Scripting)
|
||||
|
||||
_08/2023 - 04/2024_ ~ **Development lead (part-time) for prototype of Project Dual**
|
||||
|
||||
- Initial set-up of prototype in Unreal Engine 5.2
|
||||
- Development of proof-of-concept of TeamManager subystem (C++/Unreal Engine)
|
||||
- Creation of various game elements and levels (Unreal Engine Blueprints)
|
||||
- Involved with technical oversight and support, advice for production version of game
|
||||
|
||||
_01/2023 - 08/2023_ ~ **Online game client engineer for VAIL VR (AEXLAB)**
|
||||
|
||||
- Migrating to Armada game server orchestration; client, server and CI/CD side implementation
|
||||
- Implementation of various SDKs into game client and server code (C++/Unreal Engine)
|
||||
- Guiding adoption and implementation of AccelByte services into the game (C++/Unreal Engine)
|
||||
- Zero-downtime migration of PlayFab to AccelByte for authentication and entitlements (Golang)
|
||||
- Implemented game server player progression: stats tracking/uploads, MMR, rewards (C++)
|
||||
- Development lead for server browser, matchmaking, player progression and marketplace
|
||||
|
||||
#### CoolGames B.V.
|
||||
|
||||
_05/2022 - 10/2022_ ~ **Software and data engineer**
|
||||
|
||||
Migration of millions of player accounts across multiple games to a new backend system, with near-zero downtime. Built custom data export/import pipelines between Gamesparks and Nakama, with high performance. In addition, I made several DevOps/SRE improvements and supporting tools.
|
||||
|
||||
#### Bytecode Digital Agency B.V.
|
||||
|
||||
_05/2018 - 03/2022_ ~ **Co-founder and techlead**
|
||||
|
||||
Together with a business partner, I founded Bytecode Digital Agency. Our office was located in Delfgauw. Our team specialized in custom application development, mostly for software start-ups targeting web and mobile.
|
||||
|
||||
As technical lead, my activities were mostly focused on building the network layers between different applications, as well as hands-on development on back-end applications. Additionally, I worked on internal software, cyber security and cloud infrastructure. In August 2021, I announced my exit from Bytecode.
|
||||
|
||||
#### VNG Realisering, Team NLX
|
||||
|
||||
_08/2021 - 12/2021_ ~ **Senior software engineer**
|
||||
|
||||
I worked on the NLX project, an open-source software project that allows data sharing between organisations in a safe, secure and privacy-proof way. This included work on the back-end (Go), front-end (React) and SRE (Kuebernetes, Helm), improvements in development and build setup to improve development experience and speed. I have introduced Typescript into the React codebase and build systems and provided React and Typescript training to back-end developers. Part of my contributions are public [on GitLab](https://gitlab.com/commonground/nlx/nlx/-/commits/master?search=Luciano).
|
||||
|
||||
#### Airchip B.V.
|
||||
|
||||
_02/2021 - 11/2021_ ~ **CTO, ad interim**
|
||||
|
||||
Airchip was a startup that aimed to revolutionize digital ordering of drinks at festivals, even without internet connection. At Airchip, I fulfilled the role of CTO on an ad-interim basis (via Bytecode). My responsibilities included hands-on development of the back-end application (Go) and taking care of the site reliability engineering. I also had an advisory role for the three native mobile applications (Kotlin, Swift) and the BLE-protocol.
|
||||
|
||||
#### Dearly B.V.
|
||||
|
||||
_06/2020 - 11/2021_ ~ **Partner and techlead, ad interim**
|
||||
|
||||
Dearly is an initiative to help people with deal with grief and making grief more accessible to talk about, both in private and business setting. Similarly to Yourpwr, I was responsible for the technical side of the Dearly platform and advisor.
|
||||
|
||||
#### Youngpwr B.V.
|
||||
|
||||
_02/2020 - 08/2021_ ~ **Partner and techlead, ad interim**
|
||||
|
||||
Youngpwr is an initiative to support the development of entrepreneurial skills in young people. I was responsible for the technical side of the Youngpwr platform, which included software-architecture, infrastructure, systems integration, data engineering, security and code quality. Part of the management team (board member) and technical advisor.
|
||||
|
||||
#### Eneco / Dept Agency
|
||||
|
||||
_05/2019 - 03/2020_ ~ **Software engineer, ad interim**
|
||||
|
||||
Leading frontend developer for De Omschakelaar project at Eneco, an innovative tool that played a crucial role in the customer journey and promoted in several big marketing campaigns. I created several code generation tools and tackled technical debt. The technology stack used is mainly based on Typescript/Javascript and ReactJS.
|
||||
|
||||
#### Stager Software
|
||||
|
||||
_12/2017 - 05/2018_ ~ **Full-stack software engineer**
|
||||
|
||||
During my time working at Stager, I worked as a full-stack (with focus on the front-end) developer on the main Stager ticketing application. Technologies I used here were mostly ReactJS and Play! Framework (Java).
|
||||
|
||||
#### Nooijen Web Solutions
|
||||
|
||||
_05/2015 - 05/2018_ ~ **Owner, founder and developer**
|
||||
|
||||
Nooijen Solutions is a web designing/develop business that aims to creates beautiful websites and web apps. I worked here as one of the main developers (Wordpress, html, css, php, js, server-work) and I also do all of the administrative work.
|
||||
|
||||
#### Chemiewinkel
|
||||
|
||||
_07/2014 - 12/2018_ ~ **Owner and founder**
|
||||
|
||||
In the summer vacation of 2014, I started Chemiewinkel. The reason why I started a webshop in lab supplies as a 15-year-old, was because I was (and still am!) fascinated by chemistry, and supplies for hobby chemists were very hard to come by. I built and managed the webshop.
|
||||
|
||||
## Volunteer experience
|
||||
|
||||
#### Bookclub Philosophy
|
||||
|
||||
_06-2021 - present_ ~ **Reading group leader, moderator**
|
||||
|
||||
Leading the reading groups on Stoicism, Aristotle and general book discussion group.
|
||||
|
||||
Other resonsibilities include Discord server administration, development and maintenance of the [custom-made bot](https://github.com/lucianonooijen/socrates-discord-bot), helping out new members and automating tasks.
|
||||
|
||||
#### Gaga Animal Care
|
||||
|
||||
_04/2017 - present_ ~ **Board member, webmaster**
|
||||
|
||||
Volunteer for the dog shelter in Greece. Involved with planning and executing multiple neutering programs each year.
|
||||
Other responsibilities include website creation and maintenance.
|
||||
|
||||
#### Pijnackerse Watervienden
|
||||
|
||||
_2013 - 2015_ ~ **Swimming tutor assistant**
|
||||
|
||||
Leading and assisting swimming lessons for young children.
|
||||
|
||||
#### Atlas Animal Project
|
||||
|
||||
_10/2013 - 12/2015_ ~ **Volunteer**
|
||||
|
||||
Occasionally assisting Atlas representatives, usually at Schiphol airport with dogs that arrived or collecting dog crates and returning them to Kos via cargo.
|
||||
|
||||
#### Animal Rescue Kos
|
||||
|
||||
_03/2013_ ~ **Documentary director**
|
||||
|
||||
Assisting volunteers living on Kos and filming the activities. After filming I edited, released and promoted the documentary.
|
||||
|
||||
<!--Finished documentary: bit.ly/arkfilm or bit.ly/arkfilmgermany-->
|
||||
@@ -1,101 +0,0 @@
|
||||
<style>
|
||||
.gametitles-fallback { display: none }
|
||||
.gametitles {
|
||||
display: block;
|
||||
width: min(160rem, 90vw);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.gametitles-fallback { display: block }
|
||||
.gametitles { display: none }
|
||||
}
|
||||
</style>
|
||||
|
||||
<p class="gametitles-fallback"><i>Mobile not supported</i></p>
|
||||
|
||||
<table class="gametitles">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Game</th>
|
||||
<th>Genre</th>
|
||||
<th>Studio/Publisher</th>
|
||||
<th>Technology</th>
|
||||
<th>Position/work</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>AA</td>
|
||||
<td><i>NDA</i></td>
|
||||
<td>MMO</td>
|
||||
<td><i>NDA</i></td>
|
||||
<td>Go, React, UE5</td>
|
||||
<td>Staff engineer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AA</td>
|
||||
<td>Storm Lancers</td>
|
||||
<td>Roguelite</td>
|
||||
<td>Probably Monsters</td>
|
||||
<td>C++, UE5</td>
|
||||
<td>Gameplay engineer (controller input)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AAA</td>
|
||||
<td>Project Ghost</td>
|
||||
<td>MMORPG</td>
|
||||
<td>Fantastic Pixel Castle</td>
|
||||
<td>C++, Modified UE5</td>
|
||||
<td>Senior backend/SRE engineer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AA</td>
|
||||
<td><i>NDA</i></td>
|
||||
<td>Survival/PvP</td>
|
||||
<td><i>NDA</i></td>
|
||||
<td>UE5, C++ </td>
|
||||
<td>Game client AI bot engineer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AAA</td>
|
||||
<td>Gears of War: E-Day</td>
|
||||
<td>Shooter</td>
|
||||
<td>The Coalition</td>
|
||||
<td><i>NDA</i></td>
|
||||
<td>Online and Platform Services (tech lead)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Indie</td>
|
||||
<td>Project Dual</td>
|
||||
<td>Platformer</td>
|
||||
<td>Companion Group</td>
|
||||
<td>UE5, C++</td>
|
||||
<td>Lead developer prototype</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AAA</td>
|
||||
<td><i>Unannounced</i></td>
|
||||
<td>PvP/PvE</td>
|
||||
<td>Amazon Games</td>
|
||||
<td>UE5, C++, .NET</td>
|
||||
<td>Game backend engineer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AA</td>
|
||||
<td>VAIL VR</td>
|
||||
<td>VR FPS</td>
|
||||
<td>AEXLAB, Meta</td>
|
||||
<td>UE4/5, C++</td>
|
||||
<td>Online Client Engineer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AA</td>
|
||||
<td>Several</td>
|
||||
<td>Casual</td>
|
||||
<td>Coolgames</td>
|
||||
<td>Nakama/HTML5</td>
|
||||
<td>Data migration engineer</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,41 +0,0 @@
|
||||
## Technologies used
|
||||
|
||||
The technologies I'm currently working with the most and am most familiar with are in **bold**.
|
||||
|
||||
### Game development
|
||||
|
||||
- Client: **Unreal Engine 5** (C++, Blueprints), C, C++
|
||||
- Backend: **C++** (Unreal Engine, custom), **C#** (.NET), **Go**
|
||||
|
||||
### Backend/server development
|
||||
|
||||
- **Golang** (Gin, Echo, Stdlib)
|
||||
- Typescript (NodeJS, Deno, Bun)
|
||||
- C# (.NET)
|
||||
- Elixir (Phoenix, OTP)
|
||||
|
||||
_Most often with **PostgreSQL** as the underlying database._
|
||||
|
||||
### Front-end and mobile development
|
||||
|
||||
- **React with Typescript** (NextJS)
|
||||
- Web Assembly (Golang, Zig, Odin, Jai, C/C++)
|
||||
- React Native (Expo or bare)
|
||||
- Progressive Web Apps
|
||||
- Elm
|
||||
|
||||
### Site Reliability Engineering and DevOps
|
||||
|
||||
- **Linux** (Ubuntu, Debian, Arch, CentOS)
|
||||
- **CI/CD** (GitLab, GitHub Actions, Azure DevOps, Jenkins)
|
||||
- Cloud (DigitalOcean, AWS, Azure, GCP)
|
||||
- Docker and Kubernetes (Compose, Helm)
|
||||
- Infrastructure and provisioning (Ansible, Terraform, Shell)
|
||||
- Systems integration
|
||||
- Nginx
|
||||
|
||||
### Domain specific
|
||||
|
||||
- **Compilers, parsers, DSLs**: Golang, C, C++
|
||||
- Systems programming: C, C++, Zig, Odin, Jai
|
||||
- Data science: SQL, Python, Javascript/Typescript
|
||||
@@ -1,15 +0,0 @@
|
||||
## Working method
|
||||
|
||||
_Methods I employ to deliver elegant, efficient, readable, understandable and maintainable code that adds value to a company and/or product._
|
||||
|
||||
- "Always choose the best tool for the job"
|
||||
- Cloud and application structure using [The Twelve-Factor App](https://12factor.net/)
|
||||
- Software architecture via a simplified version of the principles of [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html), [DDD](https://en.wikipedia.org/wiki/Domain-driven_design) and [Layered Architecture](https://en.wikipedia.org/wiki/Multitier_architecture).
|
||||
- Prevention of [technical debt](https://en.wikipedia.org/wiki/Technical_debt), short term and long term
|
||||
- Everything (even [infrastructure as code](https://en.wikipedia.org/wiki/Infrastructure_as_code)) in version control (Git/Perforce)
|
||||
- Project management via SCRUM/Agile principle, without unnecessary meetings
|
||||
- Bringing together Development, Security and Operations through [DevSecOps](https://www.devsecops.org/) and [secure by design](https://en.wikipedia.org/wiki/Secure_by_design).
|
||||
- Test-Driven Development (TDD) of domain logic where it makes sense
|
||||
- Versioning according to [Semantic Versioning](https://semver.org/)
|
||||
- Adhere to the [ACM Code of Ethics](https://www.acm.org/code-of-ethics)
|
||||
- Use of free (open-source) software where possible, contributing when possible
|
||||
@@ -47,14 +47,18 @@ unsafe = true
|
||||
weight = 2
|
||||
url = "/expertise/"
|
||||
[[menu.main]]
|
||||
name = "Musik & Fotografie"
|
||||
name = "Musik"
|
||||
weight = 3
|
||||
url = "/experience/"
|
||||
url = "/musik/"
|
||||
[[menu.main]]
|
||||
name = "Fotografie"
|
||||
weight = 4
|
||||
url = "/fotografie/"
|
||||
[[menu.main]]
|
||||
name = "Blog"
|
||||
weight = 4
|
||||
weight = 5
|
||||
url = "/blog/"
|
||||
[[menu.main]]
|
||||
name = "Kontakt"
|
||||
weight = 5
|
||||
weight = 6
|
||||
url = "/contact/"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
+++
|
||||
date = "2025-10-25"
|
||||
title = "Behinderung und Gesellschaft"
|
||||
slug = "behinderung-und-gesellschaft"
|
||||
tags = ["Behinderung", "Gesellschaft"]
|
||||
categories = ["Gesellschaft", "Blog"]
|
||||
[params]
|
||||
metadescription = 'Gedanken zu Behinderung und Gesellschaft'
|
||||
metakeywords = 'Behinderung, Gesellschaft, blog'
|
||||
+++
|
||||
|
||||
## Behinderung und Gesellschaft
|
||||
|
||||
Mit dem Begriff der Behinderung bin ich in unserem Sprachgebrauch aktuell nicht einverstanden. Wenn wir sagen, ein Mensch "ist" behindert, so geben wir zu diesem Menschen eine Bewertung ab. Wir bilden damit eine Klassifikation. Wir benennen diese Menschen aus unserer Masse, unserer Gesellschaft heraus als "behindert".
|
||||
|
||||
Dadurch packen wir sie in mentale Käfige und grenzen diese Menschen von anderen alleine durch die bewertende Benennung ("Du bist behindert") aus und ab. Entscheidend ist für mich dabei vor allem das Wort "ist". Wir können auf die Bewertung des Individuums verzichten und das "ist" durch ein "wird" ersetzen: "ein Mensch wird behindert". Behinderte Menschen sind nicht behindert, sie werden behindert!
|
||||
|
||||
Ich bewerte den einzelnen Menschen damit nicht. Ich klassifiziere diesen Menschen damit nicht. Vielmehr sage ich damit, dass Menschen wegen ihrer Individualität von unserer Gesellschaft behindert werden. Ihre Eigenheiten und individuellen Bedürfnisse finden in unserer Gesellschaft keine, oder keine ausreichende Unterstützung. Unsere Gesellschaft ist nicht auf "Individualität" ausgerichtet, sondern auf "Normierung" und "Konformität". Wer nicht in diese Norm passt wird u.A. heute als behindert gewertet.
|
||||
|
||||
Meiner Meinung nach sollten wir eher unsere Gesellschaft bewerten als die einzelnen Menschen! Wenn wir unsere Einstellung, unsere Glaubenssätze hier anpassen können und uns eine Gesellschaft vorstellen können, die die Individualität von Menschen nicht bewertet sondern alle gleichberechtigt mit einschließt und unterstützt, dann können wir diese vielleicht auch ändern. Wäre das nicht toll ...
|
||||
|
||||
Thomas Sindt 2025
|
||||
!
|
||||
@@ -1,22 +0,0 @@
|
||||
+++
|
||||
date = "2020-07-18"
|
||||
title = "Hello, world!"
|
||||
slug = "hello-world"
|
||||
tags = [""]
|
||||
categories = ["Updates"]
|
||||
[params]
|
||||
metadescription = 'Hello world!'
|
||||
metakeywords = 'luciano nooijen, blog'
|
||||
+++
|
||||
|
||||
## Welcome
|
||||
|
||||
Big thanks for checking out my personal blog!
|
||||
|
||||
As of today, my website is completely in English and I've also added [my learning page](/learning) on here, where I tell about my learning path to try and become a self-taught computer scientist.
|
||||
|
||||
With this, I've also added this blog on my website, where I'll be posting from time to time about my experiences in learning, or to share information I hope will be valuable to others.
|
||||
|
||||
I'm also thinking about giving my site an overhaul, to improve the styling, readability and responsiveness and to add some features, but until this website is booming with traffic, I think this site will be more than just fine.
|
||||
|
||||
Anyway, that's it for now!
|
||||
@@ -0,0 +1,53 @@
|
||||
+++
|
||||
date = "2025-06-12"
|
||||
title = "Grundlagen der Sprache und der Kommunikation"
|
||||
slug = "grundlagen-der-sprache-und-der-kommunikation"
|
||||
tags = ["Sprache", "Kommunikation", "Gesellschaft"]
|
||||
categories = ["Gesellschaft", "Blog"]
|
||||
[params]
|
||||
metadescription = 'Grundlagen der Sprache und der Kommunikation'
|
||||
metakeywords = 'Sprache, Kommunikation, Gesellschaft, blog'
|
||||
+++
|
||||
|
||||
Grundlagen der Sprache und der Kommunikation
|
||||
|
||||
Seit einiger Zeit beschäftige ich mich sehr intensiv mit dem Thema Kommunikation. Es ist ein sehr wesentlicher Baustein unseres Beziehungslebens. Ich habe mir viele Gedanken dazu gemacht, wo bestimmte Glaubenssätze und Einordnungen und Normen in unserer Gesellschaft entstanden sind. Dazu gibt es viele Bücher, die sich u.a. mit den gesellschaftlichen Normen auseinandersetzen und diese auch in Frage stellen. (siehe z.B. [@simon2021] oder auch [@hardy2020] und [@karig2018]). Hier wird viel auf die gesellschaftlichen Normen geschaut und diese werden auch in Frage gestellt und diskutiert.
|
||||
|
||||
Aber erst im Buch [@guemuesay2020] wurde ich auf die wesentlichen und grundlegenden Aspekte aufmerksam, die zu einer entsprechenden Kategorisierung von Beziehungsmustern und Lebensmodellen (und noch viel mehr) führen. Es ist die Sprache der Masse, welche die Realität und die Normen, bzw. Kategorien schafft und es zum Teil sehr schwer macht, außerhalb des Erlebens dieser Masse Beziehungen (und auch vieles andere) zu denken und zu benennen.
|
||||
|
||||
Grundlegend beschreibt und benennen wir mit Sprache unsere Wahrnehmung. Aber nur, wenn die Sprache mächtig genug ist, können wir das Wahrgenommene auch benennen. Daraus folgt auch, das, wenn ich etwas benennen kann, ich dieses auch besser wahrnehmen kann.
|
||||
|
||||
Sprache ist der Stoff, aus dem unser Denken (und damit auch unser Handeln, siehe auch [@robbins2017a]) geformt wird, ohne das wir uns ggf. der Grenzen der Sprache bewusst sind. Wir betrachten die Welt durch einen eingeschränkten Blickwinkel auf Sprache und es ist wichtig, sich der Existenz solcher Grenzen bewusst zu machen, um sich vor starren Prämissen, Grundannahmen und Glaubenssätzen (siehe [@karig2018] und [@imlau2022]) zu schützen. „Das Bewusstsein für Grenzen relativiert die Dinge, die wir ignorant voraussetzen. Die Dinge, die wir als universal postulieren definieren sie doch nichts mehr als die Grenzen unseres Horizonts."
|
||||
|
||||
Sprache wird von unbenannten Benennenden geformt und kuratiert (die Masse). Sie besitzen die Macht andere Menschen und auch Themen zu benennen und somit zu expliziten, besonderen zu machen. Damit üben die Benennenden die Macht über das Benannte aus! Dieses kann sich z.B. auch in entsprechend benannte Lebensmodelle etablieren, die von den Benennenden als Norm oder als Ausgrenzung genutzt werden können. Es kommt zu einer Klassifikation der benannten Lebensmodelle.
|
||||
Wenn die Benannten oder das Benannte nicht mehr genau zu ihrer Kategorie passen, dann merken sie/diese die Grenzen der Käfige, die durch die Definition mit Sprache geschaffen wurden. Sie merken die Ablehnung der Unbenannten Benennenden, die sie nicht mehr einfach benennen (kategorisieren) können.
|
||||
Im Kontext von Beziehungsmodellen kann das z.B. bedeuten, dass es zu Ausgrenzungen und Kategorisierungen von Menschen kommen kann, weil sie nicht nach der benannten Norm der benennenden Masse leben (wollen/können/möchten) (siehe auch [@simon2021] oder auch [@karig2018]).
|
||||
|
||||
Neue, außerhalb der Erfahrung der Masse liegende Lebensmodelle führen somit in Teilen der Gesellschaft Ablehnung und Unverständnis. Kategorien und partikuläre Sichten der Benennenden werden in Sprache etabliert, indem Begriffe wie „universell“, "neutral", „rational“ und/oder „objektiv" verwendet werden. Somit werden aus eigentlich individuellen Perspektiven vermeintlich „universelle“ Einsichten, die in einer Gesellschaft verfestigt werden (böswillig könnten das auch Vorurteile sein).
|
||||
|
||||
Wir Menschen brauchen in der Kommunikation allerdings Kategorien um über die Welt sprechen zu können. Die Welt braucht sie nicht! [@guemuesay2020].
|
||||
Kategorien werden dann zu Käfigen, wenn sie mit einem Absolutheitsglauben als universell und vollständig betrachtet werden und wir uns bei der Schaffung der Kategorien nicht unserer limitierten Sichtweise bewusst sind! Dieser Anspruch führt zu Ideologien und entsprechenden Kulturen, die daraus Macht ableiten und über die Kategorisierung legitimieren! Wir alle halfen dann diese Kategorien für Umfassend, weil wir in eine entsprechende Kultur hinein geboren worden und uns diese Kategorisierungen als universal erzogen werden (siehe Glaubenssätze [@imlau2022]). Wir werden uns dann der Grenzen nicht mehr bewusst!
|
||||
|
||||
Gerade im Kontext von Lebensmodellen (aber bei weitem nicht nur da; z.B. auch bei Rassismus ist das ein ganz wesentliches Thema) führt dieses in der Kommunikation der Masse zu Ablehnung von neuen Ideen von Lebensmodellen jenseits der Monogamie (wie bereits oben genannt).
|
||||
|
||||
Um das zu Ändern ist es wichtig, zu erkennen, dass das Aufzeigen unser aller Begrenztheit durch unsere Sprache nicht als Angriff gewertet wird, sondern von allen Menschen erstrebenswert ist, sich dieser Grenzen bewusst zu werden, um sie überwinden zu können! [@guemuesay2020].
|
||||
Objektivität gibt es nicht, können nur Näherungen auf Basis vieler Perspektiven geschaffen werden. Hierzu ist es notwendig, das freie Sprechen zu ermöglichen und in der Gesellschaft auch durchzusetzen. Freies Sprechen setzt die eigene Existenz ungefragt voraus. Es muss sich nicht erklärt oder verteidigt werden. Wenn wir nicht mehr mit den Augen "anderer auf uns selbst blicken, dann sind wir frei".
|
||||
Wir werden erst alle frei sein, wenn wir uns vom Absolut-Anspruch unserer Perspektive verabschieden und keine Perspektive steht über einer anderen.
|
||||
|
||||
Damit andere Lebensmodelle in der Gesellschaft akzeptiert und auch integriert werden, ist es notwendig, dieses freie Sprechen über Lebensmodelle (und auch über vieles andere) als Standard zu etablieren. Leider ist unsere Gesellschaft noch weit von so einem Ideal entfernt.
|
||||
|
||||
Wieso haben so viele Menschen denn Angst und fühlen vielleicht auch Hass gegenüber dem Neuen und vielleicht auch Unbekannten?
|
||||
Hass und Angst wird durch Entmenschlichung möglich, die durch rohe Sprache erzeugt werden kann, aber auch durch Abstraktion von Menschengruppen hinter der einzelne Menschen verschwinden. Diese Abstraktion erfolgt durch Kategorien (siehe [@guemuesay2020])!
|
||||
|
||||
Kennen wir den Menschen als Individuum, so können wir diesen nicht mehr abstrahieren. Wir können Menschen, die wir kennen, nicht mehr nicht sehen. Wir haben Angst vor dem Neuen und Unbekannten, was uns daran hindert, unsere Sprache zu erweitern, bzw. Neues in die Sprache aufzunehmen. Damit manifestieren wir unsere Grenzen und beschneiden unsere Möglichkeiten, Neues zu erleben und zu kommunizieren.
|
||||
|
||||
Vielleicht ist es auch deswegen so schwer über Liebe, Sex, Polyamorie und andere Themen zu sprechen, da wir durch die Kategorisierung Ausgrenzungen vornehmen oder auch mangels Erfahrung und mangels sprachlicher Möglichkeiten jedes Einzelnen, bzw. dessen Perspektiven zu eingeschränkt sind. Dadurch schaffen wir durch Sprache eine in Teilen eben nicht wertschätzende Kommunikation über das Anders sein, was bereits eine Ausgrenzung darstellt.
|
||||
|
||||
Thomas Sindt 03.03.2025 Achim
|
||||
|
||||
Erweiterung:
|
||||
Nach [@robbins2017a] :
|
||||
Je besser wir nach außen kommunizieren können, unsere Wünsche, Visionen, Gedanken, Ideen und Gefühle wir kommunizieren können, desto erfolgreicher werden wir (hierzu ist natürlich auch wieder Sprache notwendig). Dadurch steigert sich auch das innere Gefühl, Erfolg zu haben und äußert sich in Form von Glück, Freude und Begeisterung. Diese Gefühle sind ein Resultat, wie ich die Ergebnisse der äußeren Kommunikation im Inneren wahrnehme und sie mir dort in Form von innerer Kommunikation manifestiere.
|
||||
Ich schaffe mir die Realität durch Kommunikation, durch Sprache. Hier wird der Bogen zu [@guemuesay2020] gespannt, in der ebenfalls postuliert wird, wenn auch auf anderen Ebenen, Sprache schafft Realität.
|
||||
|
||||
Thomas Sindt 12.06.2025 Freiburg
|
||||
@@ -1,46 +0,0 @@
|
||||
+++
|
||||
date = "2020-07-28"
|
||||
title = "How the corona pandemic might permanently change our work-attitude"
|
||||
slug = "lasting-effects-corona-pandemic-on-work-attitude"
|
||||
tags = ["Covid19", "Work from home"]
|
||||
categories = ["Society"]
|
||||
[params]
|
||||
metadescription = 'Lasting effects of the corona pandemic'
|
||||
metakeywords = 'covid, covid19, work from home'
|
||||
+++
|
||||
|
||||
_This article is a translated and slightly modified version of the article I originally wrote in Dutch for Bytecode. To read the original, click [here](https://web.archive.org/web/20210303031257/https://bytecode.nl/insights/coronacrisis-positief-effect/)_.
|
||||
|
||||
Whether we like it or not, the corona pandemic is far from over. Even though quite some countries are slowly going back to "normal" (albeit with some more distance between each other), others have yet to reach the peak of their first wave.
|
||||
|
||||
We should not forget that, for most people, it will take a while to go back to the large office buildings, nearly hugging each other in the elevator while going to the meeting with everyone in person, cramming 8 people into a tiny room. Should we actually go back to this?
|
||||
|
||||
## Working before corona times
|
||||
|
||||
If you think about it, the current mindset about work is pretty crazy. Why do I have to be in the office to do work that I might be able to do a lot more efficiently in other places? Why do I still have to go to the lecture hall, where I can't skip parts I already know and where I can't rewind if I want to hear something again (without delaying the lecture for others)?
|
||||
|
||||
Before the corona crisis, few people were lucky enough to have the opportunity to decide their own approach to work. Mostly, remote work is only allowed when ill. Starting earlier in the morning, so that you can finish earlier in the day? No chance of success. Starting later and finishing later because you're not a morning person? Certainly not.
|
||||
|
||||

|
||||
|
||||
## Our mindset about working can be improved
|
||||
|
||||
Is it really necessary to work in this "pre-corona" way? Do you have to be in the office for everything? Of course not everyone can (or wants to) be a [digital nomad](https://en.wikipedia.org/wiki/Digital_nomad), flying across the world to work online in hotels. However, there are possibilities to make work more enjoyable for everyone. I don't envision a shelf filler, car mechanic or electrician who works from home at times that suit him. But a translator, consultant or marketeer?
|
||||
|
||||
Personally, I am very attracted to the ideas from the [async manifesto](http://asyncmanifesto.org/), a set of insights on how software development can be better implemented. The ideas come down to this: use modern tools, create a flexible work environment, do not disturb people's concentration unless it's urgent and only hold meetings when it's really necessary. The async manifesto is focused primarily on software developers, but I think anyone with a non-physical profession can benefit from the tips given here.
|
||||
|
||||
When it comes to productivity, it seems that working from home (in a quiet office) is a lot [more productive](https://www.inc.com/marcel-schwantes/new-study-reveals-why-working-from-home-makes-workers-more-productive.html) than working at the office. It is fairly easy to understand why this is the case for many people. Every time you are interrupted in a "[deep-work](https://www.nrc.nl/nieuws/2016/03/30/de-superkracht-van-de-21ste-eeuw-1603307-a382406)" session, it takes [25 minutes](http://blog.idonethis.com/distractions-at-work/) to get your focus back. Not to mention the "this-could-have-been-an-email"-meetings that take an hour each.
|
||||
|
||||
How great would it be, instead of being constantly interrupted for some trivial question, to just answer non-urgent questions a few times per day? To not be disturbed by people who talk too loudly about yesterday's soccer game? As Jason Fried explains in his [TED talk](https://www.ted.com/talks/jason_fried_why_work_doesn_t_happen_at_work), you have to be able to work long uninterrupted stretches to really get something done.
|
||||
|
||||

|
||||
|
||||
## The corona pandemic offers an opportunity to reflect on the current way of thinking
|
||||
|
||||
Now that a large part of us is still working from home due to the corona crisis, we have a chance to see what really works for us. Is that 9-to-5 job really ideal, or does a 10-to-6 or 7-to-3 job fit better, perhaps? Or why not 7-to-4 with an hour's break to run a lap? And if 9-to-5 office-based work turns out to suit you best, that's fine too, of course!
|
||||
|
||||
How am I actually most productive? And more importantly, how do I actually get the most satisfaction from my work? These are questions to which we will be able to find an answer in the near future, because we now have room for experimentation.
|
||||
|
||||
Hopefully the changes in (higher) education will also have a lasting effect, because of the long-awaited technological innovation.
|
||||
|
||||
I'm not in favour of closing down all the offices and changing to remote work as a standard, which is naturally unfeasible. For example, I also think that face-to-face meetings work better than talking on the phone. But I believe that - with companies large and small - a lot can be improved compared to the current situation, if the right balance can be found. Let's hope that this will be a positive lasting effect of this corona crisis.
|
||||
@@ -1,105 +0,0 @@
|
||||
+++
|
||||
date = "2020-08-24"
|
||||
title = "Runtime JSON typechecks with Typescript interfaces"
|
||||
slug = "runtime-json-typechecks-with-typescript-interfaces"
|
||||
tags = ["Typescript", "Code Generation"]
|
||||
categories = ["Frontend"]
|
||||
[params]
|
||||
metadescription = 'How to add runtime typechecks for JSON in Typescript'
|
||||
metakeywords = 'typescript, json, typechecking'
|
||||
+++
|
||||
|
||||
_The contents of this article are technical in nature and mainly written for software developers_
|
||||
|
||||
Bytecode is currently working on a project for a startup. We are developing an app that is linked to a backend data storage service. For this project, I wanted to have more security around the API calls and validate the types at runtime.
|
||||
|
||||
## Background
|
||||
|
||||
At Bytecode, we use Typescript extensively for front-end and mobile development, to prevent Javascript's liberal dynamic typing system from causing errors. Typescript adds an extra layer of security. In recent years, we have seen a strong decrease in type-related errors due to the use of Typescript.
|
||||
|
||||
However, Typescript also has it's limitations, the main one being that types and interfaces disappear during compile-time. So there are no run-time checks on external data that is not available during compilation, like for example API responses. Ideally, Typescript would support marshalling in a way [similar to Go](https://medium.com/rungo/working-with-json-in-go-7e3a37c5a07b), to secure type safety during runtime. Unfortunately, however, the Typescript layer disappears during compile-time and only Javascript remains: code that knows nothing about the types and interfaces defined in the source code. Unfortunately, the Go-like approach would never be possible using features built into the language.
|
||||
|
||||
Until recently, we did our API calls directly within Redux actions, which didn't cause any problems for small applications. However, this setup is not the best, considering the [single-responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle) and keeping in mind that Redux actions can get confusing with large projects, if you're not careful. This is why we recently switched to creating separate API packages as an abstraction layer on top of the API calls. We now only call a function that executes the API calls and checks HTTP errors. If there are no errors, we'll get the data back. If there are errors, an error is thrown. This way, the Redux code does not know anything about the details of the API call.
|
||||
|
||||
Another reason to use a separate API package, is our desire to be able to set up an SDK more easily later on, when we start targeting another platform (think of an application, first only web, but where a mobile app will be added as well). It will then be possible to use shared code easily, without duplicate logic. Since these API packages become increasingly important with more dependent applications, the run-time guarantees also get more crucial. If the SDK says that a function returns a certain data type, we also want to guarantee this or else give an error message.
|
||||
|
||||
## Requirements and research
|
||||
|
||||
Our research question consisted of several parts:
|
||||
|
||||
- How can we generically check a JSON object against a Typescript interface, without the need for duplicate code for type definitions?
|
||||
- How can we achieve the above without having to modify other production code outside the API package to make this check possible?
|
||||
- How can this be done in NodeJS, React Native and in the browser?
|
||||
|
||||
There are enough libraries that make it possible to check a JSON structure, based on a DSL (domain specific language). However, this wasn't what we were looking for, because we were already using Typescript and didn't want to maintain the same type definition in multiple ways. We would prefer to develop a solution where no code generation or extra step in compilation is needed, but everything on-the-fly during runtime (like Go).
|
||||
|
||||
A few months ago, I read a [blog post by Picnic](https://blog.picnic.nl/guarding-a-react-native-application-from-evil-json-6f7cbb4404de), describing their project "Aegis", in which they had offered a solution to this problem. However, I noticed that it was still difficult to implement. The code is open source, but there was no example of implementation on a larger scale, because this was done within Picnic's proprietary app. A code generation step was also needed. We would, if possible, prefer not to have this extra step.
|
||||
|
||||
On the Subreddit of Typescript I had placed a [post](https://www.reddit.com/r/typescript/comments/i8yk6i/validating_objects_type_at_runtime/), where I submitted my question. I primarily received responses with examples of code generation solutions. Some responses discussed runtime solutions, but these solutions were unnecessarily complex and/or required modifications within the build configuration of Typescript. We prefer to avoid this, because we prefer to keep something experimental separate from the rest of our production code, so that if we are not satisfied, we can revert the changes.
|
||||
|
||||
A possible solution that popped into my head was the following:
|
||||
|
||||
- Load all type-definitions through the file system as strings
|
||||
- Use the Typescript compiler as production dependency and parse these strings
|
||||
- Compare the result of parsing against the JSON data to see if it matches the interfaces
|
||||
|
||||
However, this would mean that a substantial part of the Typescript compiler would have to become part of the app and thus increase the bundle size. The Typescript compiler is not the fastest in the world either, so this would take a considerable amount of extra time when it has to be done on-the-fly. In addition, the filesystem is only suitable for Node.js and not for browser environments, so compatibility could not be maintained. Unfortunately this solution was not feasible.
|
||||
|
||||
Ultimately, I chose to use [Picnic's Aegis](https://github.com/PicnicSupermarket/aegis), mainly because of the simplicity of the tool and because it can be used without modifying other aspects of the project (compilation steps, configurations or production code in other parts of the application).
|
||||
|
||||
## Implementation
|
||||
|
||||
The final implementation is as follows. Within the API folder, all public types (that is, the arguments and return types of the entire API package) are defined in the `types` folder. For all types in this folder, Aegis creates decoders and stores them in the `internal` folder of the API package.
|
||||
|
||||
In order to make the aforementioned solution usable for Bytecode, a few adjustments had to be made in Aegis. For example, we added ESLint comments at the top of the file. This was done in a [fork of Aegis](https://github.com/lucianonooijen/aegis/tree/bytecode) on Github. This is the dependency used in Bytecode's project. To build the decoders, a command has been added to the `package.json` of the React-Native/Expo project. By simply running `yarn run aegis`, Aegis is called with the right arguments and all decoders are built.
|
||||
|
||||
The production code of the API package already used an internal `returnOrThrow` function, which received an internal API response type (consisting of the response of the API and/or an error if it occurred), threw an error if it existed and otherwise returned the data. This function has been modified so that a second argument is given to the function, namely the decoder. In `returnOrThrow` the decoder is then used to check the data before it is returned. See the example below:
|
||||
|
||||
```ts
|
||||
import { Decoder } from "decoders/types";
|
||||
import { guard } from "decoders";
|
||||
|
||||
interface APIResultSuccess<T> {
|
||||
data: T;
|
||||
error?: undefined;
|
||||
}
|
||||
interface APIResultFailure {
|
||||
data?: undefined;
|
||||
error: string;
|
||||
}
|
||||
type APIResult<T> = APIResultSuccess<T> | APIResultFailure;
|
||||
|
||||
const throwOrReturn = <T>(result: APIResult<T>, decoder: Decoder<T>): T => {
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
// We can assume that data is valid (type T) if no error was found
|
||||
const data = result.data as T;
|
||||
|
||||
const decodeChecker = guard(decoder);
|
||||
const _ = decodeChecker(data); // Throws if it's not valid
|
||||
return data;
|
||||
};
|
||||
|
||||
export default throwOrReturn;
|
||||
```
|
||||
|
||||
If the return body does not satisfy the decoder, an error is thrown, which can be caught when calling the API call.
|
||||
|
||||
## Wishlist
|
||||
|
||||
For now, the implementation of JSON type checks is still experimental. Within the codebase, this type check only affects a small (separate) part, so it can be easily removed later on. This is why it is not automated yet, something we would like to add later on. Automating this without modifying the build configuration can still be a challenge. Until we automate this completely, we can add a check in the CI pipeline that gives an error message when running Aegis causes file changes in Git, meaning the decoders have not been updated after making changes to the type definitions.
|
||||
|
||||
Another very good use-case of this setup would be the end-to-end testing of the API for which the package is built. The end-to-end testing of APIs is something that has been on the Bytecode wishlist for some time now. JSON type checking can also provide great added value, allowing you to immediately check whether the API returns data according to the expectations.
|
||||
|
||||
Regarding improvement to Aegis itself, at the moment Aegis is mainly built for the "happy flow", there are still some edge cases that don't quite work. The tool is now definitely usable, but improvements are still needed for large-scale use.
|
||||
|
||||
Support for Aegis configuration files is another useful addition. Now arguments for `importPath` and `outputFile` must be given as CLI options. Simply calling `aegis generate`, which then loads the configuration by itself would be a nice addition.
|
||||
|
||||
If it turns out that this workflow works very well for Bytecode, there is a good chance that we will further develop the Aegis tool ourselves, open-source of course. For now, the tool is not yet available on NPM. That would be the first step towards a stable release.
|
||||
|
||||
## Example project
|
||||
|
||||
As an addition to this article, an example project is also available, with a simple API call, where the tool can be seen in action.
|
||||
|
||||
Check out the [project on Github](https://github.com/lucianonooijen/ts-runtime-json-checks-example).
|
||||
@@ -1,16 +0,0 @@
|
||||
+++
|
||||
date = "2022-01-13"
|
||||
title = "Webinar replay: common pitfalls for software start-ups and how to avoid them"
|
||||
slug = "webinar-replay-software-start-up-pitfalls"
|
||||
tags = ["Webinar", "Start-ups", "Video"]
|
||||
categories = []
|
||||
[params]
|
||||
metadescription = 'Webinar replay for start-up pitfalls'
|
||||
metakeywords = 'start-ups, pitfalls, webinar, luciano nooijen'
|
||||
+++
|
||||
|
||||
On November 29th, 2021, I gave a webinar for Bytecode about common pitfalls for software start-ups and how to avoid them.
|
||||
|
||||
A replay of this webinar can be seen here:
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Yl2wDvipw38" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
@@ -1,18 +0,0 @@
|
||||
+++
|
||||
date = "2025-02-12"
|
||||
title = "Hello, world! Again!"
|
||||
slug = "hello-world-again"
|
||||
tags = [""]
|
||||
categories = ["Updates"]
|
||||
[params]
|
||||
metadescription = 'A short update on this blog'
|
||||
metakeywords = 'luciano nooijen, blog'
|
||||
+++
|
||||
|
||||
Recently, I have had quite a lot of ideas about interesting blog posts, some short, some long; some general notes and some more technical. All in the hope that this can be helpful or at least somewhat insightful for others.
|
||||
|
||||
I have updated my website to add slightly better blog support, including categories and tags to make this possible, plus better RSS feed support.
|
||||
|
||||
Currently, the website is quite basic. And although I have considered giving it a bit of an overhaul with better styling, I would prefer to keep this website somewhat minimalist. I might make some changes to improve readability, but the styling itself will still be very minimalistic.
|
||||
|
||||
Anyway, that's it for now!
|
||||
@@ -1,35 +0,0 @@
|
||||
+++
|
||||
date = "2025-02-13"
|
||||
title = "Easy help command for Makefiles"
|
||||
slug = "help-makefile"
|
||||
tags = ["QOL"]
|
||||
categories = ["Tools"]
|
||||
[params]
|
||||
metadescription = 'Adding a help command to your Makefile'
|
||||
metakeywords = 'makefile, help command'
|
||||
+++
|
||||
|
||||
I love Makefiles, they are my prefered way to manage builds. Not just for C or C++, I also use it for Golang for example. To me, it is the best way to manage build commands in a single place.
|
||||
|
||||
As I love CLI tools in general, and working with them, the `--help` flag is often a great help, but Make does not offer this out of the box, and navigating complex Makefiles can be challenging.
|
||||
|
||||
I can't remember where or when, but a few years ago, I found a snippet somewhere of a Makefile command that adds a coloured text output of all Makefile commands. Using it is as simple as making the following your first Makefile command:
|
||||
|
||||
```Makefile
|
||||
.PHONY: help
|
||||
help: ## Shows all commands
|
||||
@echo 'All Makefile commands:'
|
||||
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
```
|
||||
|
||||
After adding this, you can simply add comments after your other Makefile commands with two `#` characters, like this:
|
||||
|
||||
```Makefile
|
||||
.PHONY: dev
|
||||
dev: CFLAGS = $(CFLAGS_DEV) ## Development build for /src and /example
|
||||
dev: clean $(LIB) $(EXAMPLE)
|
||||
```
|
||||
|
||||
This will now be automatically generate help output when running `make help`, or when you have defined this as your first Make command, with simply `make`
|
||||
|
||||

|
||||
@@ -1,334 +0,0 @@
|
||||
+++
|
||||
date = "2025-02-14"
|
||||
title = "My favourite way to handle SQL in Golang"
|
||||
slug = "best-golang-sql-handling"
|
||||
tags = ["Golang", "SQL"]
|
||||
categories = ["Backend"]
|
||||
[params]
|
||||
metadescription = 'How to best handle SQL logic in Golang applications for migrations and queries'
|
||||
metakeywords = 'golang, sqlc, go, sql, migrations, code generation'
|
||||
+++
|
||||
|
||||
_tl;dr: using sqlc and golang-migrate allows plain easy SQL in your application_
|
||||
|
||||
Golang is a great language when building web/API services. And that makes a lot of sense, as Go was created for building scalable, performant applications. The language is very cohesive, compiles fast, has great performance and has a great ecosystem in addition to its rich standard library and great toolchain. Whenever I need to build a web service or API, I tend to reach for Go when possible, hence why I built quite a lot of applications with it so far, including around a dozen or so application back-ends.
|
||||
|
||||
But this is not an article about why Go is great, but rather on how to handle SQL. Considering Golang is not made to be used in Ruby on Rails-type frameworks, you'll most likely have to set up the SQL-logic yourself.
|
||||
|
||||
My approach to SQL handling in Golang is the result of trying out a lot of approaches and seeing what works and what doesn't. This approach is mostly aimed at building API services, backed by a database. In my case, I use Postgresql, though other SQL databases should also work with a few tweaks.
|
||||
|
||||
As most applications that I build deal with quite a lot of data, I don't want to work with ORMs like Gorm, due to the performance impact and unnecessary abstractions that it introduces. In general, I'm not the biggest fan of ORMs, as I already know SQL and don't like learning additional tools to abstract away the actual SQL code, using SQL directly for me is simply faster, both in terms of performance as well as writing the application.
|
||||
|
||||
So how then do I approach SQL in Golang?
|
||||
|
||||
## Code generation with sqlc
|
||||
|
||||
One of the things I like about the Go ecosystem is that there are a lot of code generation tools. Code generation is something that is quite underused in my opinion, and [sqlc](https://sqlc.dev/) is an amazing example of how code generation can be very powerful.
|
||||
|
||||
With sqlc, you can write plain sql, and then run a command that generates the Go code in a module you can use in your application.
|
||||
|
||||
## Configuring sqlc
|
||||
|
||||
After [installing sqlc](https://docs.sqlc.dev/en/latest/overview/install.html) on your device, getting up and running with sqlc is quite trivial. The generic installation instructions are found in the [sqlc docs](https://docs.sqlc.dev/en/latest/tutorials/getting-started-postgresql.html), but I'm using a bit of a modified configuration.
|
||||
|
||||
We start with creating the `sql/` and `migrations/` folders, for SQL queries and database migrations.
|
||||
|
||||
In the migrations directory, add a file `0000_schema.up.sql` and `0000_schema.down.sql` for adding your initial database schema, take this up migration for example:
|
||||
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
-- Enable uuid extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE users (
|
||||
-- User fields
|
||||
);
|
||||
|
||||
-- The rest of your schema
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
Make sure to add the down migrations as well. The migrations are used by sqlc to generate the database structure and add typed columns for database manipulations. They will also be used to perform actual database migrations. Wrap your migration logic in `BEGIN` and `COMMIT` so you can `ROLLBACK` on a failed migration.
|
||||
|
||||
## Full sqlc config example
|
||||
|
||||
Here is the full configuration I'm using for the Capsa API service:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
queries: "sql/"
|
||||
schema: "migrations/"
|
||||
gen:
|
||||
go:
|
||||
package: "database"
|
||||
out: "internal/data/database"
|
||||
sql_package: "pgx/v5"
|
||||
emit_json_tags: true
|
||||
json_tags_case_style: "camel"
|
||||
emit_pointers_for_null_types: true
|
||||
overrides:
|
||||
# UUID
|
||||
- db_type: "uuid"
|
||||
go_type:
|
||||
import: "github.com/google/uuid"
|
||||
type: "UUID"
|
||||
- db_type: "uuid"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "github.com/google/uuid"
|
||||
type: "UUID"
|
||||
pointer: true
|
||||
# Timestamp
|
||||
- db_type: "pg_catalog.timestamp"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
pointer: true
|
||||
- db_type: "pg_catalog.timestamp"
|
||||
go_type: "time.Time"
|
||||
# Custom
|
||||
- column: "logs_chunks.category_counts"
|
||||
go_type:
|
||||
import: "github.com/capsa-gg/capsa/server/internal/entities"
|
||||
type: "LogChunkMetadata"
|
||||
- column: "logs_chunks.severity_counts"
|
||||
go_type:
|
||||
import: "github.com/capsa-gg/capsa/server/internal/entities"
|
||||
type: "LogChunkMetadata"
|
||||
```
|
||||
|
||||
This configuration replaces the pgx (Postgres database driver) UUID and Timestamps with the Google and standard library implementations respectively. It also specifies a few column types to be used, so the returned data has better compatibility with my domain logic. For further explanation on this, see the [docs](https://docs.sqlc.dev/en/latest/reference/config.html).
|
||||
|
||||
## Writing SQL statements
|
||||
|
||||
With sqlc configured, we can write some SQL code! The [docs](https://docs.sqlc.dev/en/latest/howto/select.html) explain things in more detail, but here are two very simple examples:
|
||||
|
||||
```sql
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users
|
||||
WHERE id = $1;
|
||||
```
|
||||
|
||||
Gets a user by their ID. The `*` return value here will use the database schema to determine the return type to make it fully typesafe.
|
||||
|
||||
```sql
|
||||
-- name: UpdateUser :one
|
||||
-- Update a user with optional parameters
|
||||
UPDATE users
|
||||
SET first_name = @first_name,
|
||||
last_name = @last_name,
|
||||
user_role = @user_role
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
```
|
||||
|
||||
For updating a user, this also shows how arguments can be named with sqlc for clarity in the domain logic.
|
||||
|
||||
## Complex SQL
|
||||
|
||||
The two examples above are the 'hello world' equivalent of SQL statements. In larger applications things become more complex. An example of this is searching. Although there might be better ways, this is the way I have implemented search for Capsa, which is not too complex:
|
||||
|
||||
```sql
|
||||
-- name: SearchResources :many
|
||||
-- Searches in some database tables to find matching resources based on a "contains string" pattern (LIKE '%<arg>%')
|
||||
WITH resources AS (
|
||||
SELECT
|
||||
'Environment' AS table_name,
|
||||
lower(e.key::text) AS identifier,
|
||||
concat(e.name, ' environment for ', t.name) AS description,
|
||||
'' AS details
|
||||
FROM environments e
|
||||
LEFT JOIN titles t ON t.id = e.title
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'Logs' ,
|
||||
lower(l.log_uuid::text) ,
|
||||
concat(l.log_type, ' log on ', platform, ' (', t.name, ', ', e.name, ')'),
|
||||
count(lc)::text
|
||||
FROM logs l
|
||||
LEFT JOIN environments e ON e.id = l.environment
|
||||
LEFT JOIN titles t on t.id = e.title
|
||||
LEFT JOIN logs_chunks lc ON l.id = lc.log
|
||||
GROUP BY l.log_uuid, l.log_type, platform, t.name, e.name
|
||||
)
|
||||
SELECT table_name, identifier, description::text, details::text
|
||||
FROM resources
|
||||
WHERE identifier LIKE '%' || lower(@search) || '%'
|
||||
LIMIT sqlc.arg('limit');
|
||||
```
|
||||
|
||||
Another more complex example is filtering with optional arguments. The full statement is over 50 lines long, but here is the most important part of the implementation for Capsa:
|
||||
|
||||
```sql
|
||||
-- name: ListAvailableLogs :many
|
||||
-- Fetches all log chunks and aggregates an overview.
|
||||
-- LogUUID is an optional field used as a filter, which if set will return only a single result.
|
||||
WITH -- Omitted
|
||||
SELECT -- Omitted
|
||||
FROM logs l
|
||||
JOIN cat_counts cc ON cc.log = l.id
|
||||
JOIN sev_counts sc ON sc.log = l.id
|
||||
JOIN chunk_data cd ON cd.log = l.id
|
||||
JOIN environments e on l.environment = e.id
|
||||
JOIN titles t on e.title = t.id
|
||||
LEFT JOIN links ll on l.id = ll.source
|
||||
WHERE ( l.log_uuid = sqlc.narg(filter_by_log_uuid) OR sqlc.narg(filter_by_log_uuid) IS NULL ) -- Optionally filter by Log UUID
|
||||
AND ( e.key = sqlc.narg(filter_by_environment)::uuid OR sqlc.narg(filter_by_environment) IS NULL ) -- Optionally filter by Environment
|
||||
AND ( l.platform = sqlc.narg(filter_by_platform)::varchar OR sqlc.narg(filter_by_platform) IS NULL ) -- Optionally filter by Platform
|
||||
AND ( l.log_type = sqlc.narg(filter_by_logtype) OR sqlc.narg(filter_by_logtype) IS NULL ) -- Optionally filter by LogType
|
||||
GROUP BY l.id, t.name, e.name, cd.line_count, cd.chunk_count, cd.earliest_start, cd.latest_end, ll.sum
|
||||
ORDER BY earliest DESC
|
||||
LIMIT @fetchlimit::int;
|
||||
```
|
||||
|
||||
This will simply ignore all arguments that are not set, or include them if they are. In all honesty: the syntax with an ORM would be cleaner, but I do suspect the performance will take a hit. If performance with `ListAvailableLogs` becomes an issue, I can optimize it, with ORMs, that is a lot harder.
|
||||
|
||||
## Generating the code
|
||||
|
||||
To generate the code, simply run `sqlc generate` in the root of your project, and you should have your code! There are more commands you can use for analysis and linting, which are outlined in the [docs](https://docs.sqlc.dev/en/latest/howto/generate.html).
|
||||
|
||||
## Using the generated code
|
||||
|
||||
During the application start, you can use your config to generate the instance of the sql-generated structs:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/capsa-gg/capsa/server/internal/data/database"
|
||||
)
|
||||
|
||||
// NewDatabase initializes a Database instance.
|
||||
func NewDatabase(c *entities.Config) (*database.Queries, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Database connection
|
||||
dbConn, err := pgxpool.New(ctx, c.DatabaseConnectionString())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening database connection: %w", err)
|
||||
}
|
||||
|
||||
// Ping database
|
||||
err = dbConn.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error pinging database: %w", err)
|
||||
}
|
||||
|
||||
// Database instance
|
||||
db := database.New(dbConn)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
```
|
||||
|
||||
You now have a `*database.Queries` instance that you can use to use query the database! Using it is as simple as
|
||||
|
||||
```go
|
||||
user, err := db.GetUserByID(ctx, userId)
|
||||
```
|
||||
|
||||
There is also support for performing logic inside of a transaction, as outlined in the [docs](https://docs.sqlc.dev/en/latest/howto/transactions.html#using-transactions).
|
||||
|
||||
## Handling migrations
|
||||
|
||||
You should only be changing the database structure using migrations. We already have the migration scripts used by sqlc, which we can use to perform the database migrations.
|
||||
|
||||
For migrations, I'm using the golang-migrate package, with a small wrapper. This is the full code of the migrator package for Capsa:
|
||||
|
||||
```go
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/pgx/v5" //nolint:gocritic,stylecheck // Needs import for using pgx.WithInstance
|
||||
"github.com/golang-migrate/migrate/v4/source/httpfs"
|
||||
|
||||
// Needs side effect from pgx/v5.
|
||||
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5" //nolint:gocritic,stylecheck // Needs import for side effect
|
||||
|
||||
"github.com/capsa-gg/capsa/server/migrations"
|
||||
)
|
||||
|
||||
// Direction indicates the migration direction.
|
||||
type Direction string
|
||||
|
||||
// UpAll and DownAll indicate the migration direction.
|
||||
const (
|
||||
UpAll = Direction("UpAll")
|
||||
DownAll = Direction("DownAll")
|
||||
)
|
||||
|
||||
// New returns a migrator closure that will accept UpAll or DownAll to up or down migrate the database.
|
||||
func New(dbConn *sql.DB, dbName string) func(Direction) error {
|
||||
return func(direction Direction) error {
|
||||
// Direction check
|
||||
if direction != UpAll && direction != DownAll {
|
||||
return errors.New("migration direction should be 'UpAll' or 'DownAll'")
|
||||
}
|
||||
|
||||
driver, err := pgx.WithInstance(dbConn, &pgx.Config{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid target postgres instance, %w", err)
|
||||
}
|
||||
// Source instance for the migrations embedded in server binary
|
||||
sourceInstance, err := httpfs.New(http.FS(migrations.Migrations), ".")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid source instance, %w", err)
|
||||
}
|
||||
|
||||
// Create migrator instance
|
||||
migrator, err := migrate.NewWithInstance("httpfs", sourceInstance, dbName, driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize migrate instance, %w", err)
|
||||
}
|
||||
|
||||
// Do the actual migration
|
||||
if direction == UpAll {
|
||||
return handleMigratorErrors(migrator.Up())
|
||||
}
|
||||
|
||||
if direction == DownAll {
|
||||
return handleMigratorErrors(migrator.Down())
|
||||
}
|
||||
|
||||
return errors.New("you should not see this")
|
||||
}
|
||||
}
|
||||
|
||||
func handleMigratorErrors(err error) error {
|
||||
if errors.Is(err, migrate.ErrNoChange) { // Do not report error when no database change
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
With this, performing the database migrations is as simple as
|
||||
|
||||
```go
|
||||
import "path/to/migrator"
|
||||
|
||||
migrate := migrator.New(db, config.DatabaseName)
|
||||
err = migrate(migrationDirection)
|
||||
```
|
||||
|
||||
Now you have good 'ol plain SQL for migrations as well as database queries in Golang with code generation!
|
||||
|
||||
## Example to see this in action
|
||||
|
||||
An example of this approach in action can be found in the [Capsa API server](https://github.com/capsa-gg/capsa/blob/main/server/). Capsa is still work in progress, but I highly doubt the SQL approach will change.
|
||||
@@ -1,47 +0,0 @@
|
||||
+++
|
||||
date = "2025-02-15"
|
||||
title = "Finding the best translation for Marcus Aurelius' Meditations"
|
||||
slug = "best-translation-meditations"
|
||||
tags = ["Meditations", "Marcus Aurelius", "Stoicism"]
|
||||
categories = ["Books", "Philosophy"]
|
||||
[params]
|
||||
metadescription = "After reading around 10 different translations of Marcus Aurelius' Meditations, which translation do I like best"
|
||||
metakeywords = 'meditations, translation, marcus aurelius, waterfield'
|
||||
+++
|
||||
|
||||
I have read quite a lot of book on Greek and Roman philosophy and Marcus Aurelius' Meditations is the book I have read most often by far. In total, I own around 7 translations and I have read sections of other translations as well. In addition to this, I have read several secondary works, in addition to the sections of the Meditations that I have read in the original Greek. In 2021/2022, I have hosted an eight-month long weekly reading group on the Meditations as well.
|
||||
|
||||
One important note about reading philosophical works, is that translation is a very important aspect if it was written in a different language. There are many features present in Ancient Greek (the language that Meditations was written in) that are not present in English, or words with no English equivalent.
|
||||
|
||||
Translation is not just about translating the words but - especially with philosophy - it is also important to capture the style, tone, etc. of a text as this is essential to communicate the meaning of the source material. If that is not possible due to linguistic challenges, that should be clarified by the translator.
|
||||
|
||||
So, back to Marcus Aurelius. After reading the Meditation and several secondary sources, what is my favourite translation of the work in English?
|
||||
|
||||
## Waterfield's Annotated Edition
|
||||
|
||||
By far the best translation of the Meditations in English is _Robin Waterfield's Annotated Edition_.
|
||||
|
||||
In this translation, Waterfield does an amazing job of capturing the conciseness of the original work. The English text 'feels' very similar to the original, literary, yet direct.
|
||||
|
||||
In addition to the great translation, this edition has a great introduction, clarification on translating certain terminology of Greek philosophy, but to me the best part are the tons of annotations present as footnotes.
|
||||
|
||||
The Meditations was not a book to be published, for Marcus Aurelius, it was most likely his "commonplace book" and Waterfield does an amazing job of adding connections to other works works and even to similar passages within the book, or simply explaining cultural references.
|
||||
|
||||
If you are interested in reading Meditations, or if you have read it before but want to understand it better, you cannot go wrong with this edition.
|
||||
|
||||
The ISBNs are:
|
||||
|
||||
- Hardcover: 9781541673854
|
||||
- Paperback: 9781541673861
|
||||
|
||||
## Other translations worth checking out
|
||||
|
||||
- _If you can read Ancient Greek_: Haine's translation from the Loeb Classical Library is not an amazing translation, but has the original text on the left, and the English translation side-by-side on the right, which can be great.
|
||||
- _Dutch translation_: I have not read as many Dutch translations as English ones, but I like Simone Mooij-Valk's "Persoonlijke notities" best so far.
|
||||
- _German Translation_: there is a German translation of Waterfield's edition (Selbstbetrachtungen: Die kommentierte Edition, translated by Elisabeth Liebl), which I have partly read and quite liked.
|
||||
|
||||
<br>
|
||||
|
||||
> ἐξετάζειν τί μοί ἐστι νῦν ἐν τούτῳ τῷ μορίῳ, ὃ δὴ ἡγεμονικὸν καλοῦσι, καὶ τίνος ἄρα νῦν ἔχω ψυχήν;
|
||||
>
|
||||
> At this moment, what is occupying that part of me they call the command center? What kind of a soul do I actually have at the moment? (Waterfield's translation of Meditations 5.11)
|
||||
@@ -1,96 +0,0 @@
|
||||
+++
|
||||
date = "2025-02-23"
|
||||
title = "Why Baldur's Gate 3 is a masterpiece"
|
||||
slug = "why-baldurs-gate-3-is-a-masterpiece"
|
||||
tags = ["Review"]
|
||||
categories = ["Games"]
|
||||
[params]
|
||||
metadescription = "Baldur's Gate 3 is the best game I have ever played and why it sets a new gold standard for RPGs (no spoilers)"
|
||||
metakeywords = 'baldurs gate 3, bg3, masterpiece'
|
||||
+++
|
||||
|
||||
_This article does not contain spoilers._
|
||||
|
||||
I should preface this by saying that I have never played Dungeons and Dragons in my life, and knew very little about it before playing Baldur's Gate 3. I started playing the game because it was recommended to me by my partner and I was somewhat skeptical about it, because it was so foreign to me. It was nothing like I expected, in the best way possible.
|
||||
|
||||
I am not a huge gamer. Even though I work on games, I tend to enjoy that more than actually playing games. I am quite picky with the games I play. There are quite a lot of games that I have enjoyed a ton, but nothing comes close to Baldur's Gate 3, it is by far the best game I have ever played.
|
||||
|
||||
My first playthrough took me around 80 hours, where I took my time but didn't do most side-quests (which I didn't even know existed). My partner's playthrough is, at the time of writing, 196 hours long and we're only now moving into the end game [^finished]. I have also started a few playthroughs to explore different classes and approaches to the game at higher difficulties. It will be a long time before I'm done playing this game.
|
||||
|
||||
[^finished]: After 214.5h we finished the game on March 23, 2025.
|
||||
|
||||
For brevity, I'll abbreviate Baldur's Gate 3 to BG3 going forward.
|
||||
|
||||
## Endless choice and replayability
|
||||
|
||||
Having choices is of course a core part of RPGs, but BG3 takes this to almost an extreme. You don't feel forced to take a particular path in your playthrough and there are no predefined paths you can take.
|
||||
|
||||
Choices you make early on can have a lot of influence on later parts of the game. The options you are given are not just to get a particular response from an NPC, but actually have a huge impact on the world in later acts.
|
||||
|
||||
No two playthroughs are the same. You can play as a murderous psychopath, a charming bard, or even as a cat. Even if you pick the infamous stealth archer min-max build, it's highly likely two playthroughs will be radically different, even when you follow an Honor Mode guide to complete the game.
|
||||
|
||||
Romancing characters is done incredibly well and offers a ton of choices as well. You cannot romance all characters at once, and romancing a different companion offers even more value to start another playthrough.
|
||||
|
||||
On the topic of romance, I feel obliged to mention the 'bear scene' (if you know, you know). The fact that Larian took the time to add this as an option to me is incredible, regardless of which choice you made if you encountered this, the fact it's there is already amazing to me.
|
||||
|
||||
## Cohesiveness of the story
|
||||
|
||||
It's incredible to me how cohesive the story is. With so many different ways to do missions, get somewhere or handle situations you would think there would be many inconsistencies later in the game.
|
||||
|
||||
Sure, there might be some tiny inconsistencies here and there, but to me it's amazing how a game where the story is not linear apart from the three Acts can make the story of any playthrough feel like it was written to be played this way.
|
||||
|
||||
The overarching story of BG3 has some of the best storytelling that I have ever seen in a game, even better than most movies or TV series. Everything comes together, there are many plot twists that all make sense.
|
||||
|
||||
Everything in BG3 is interconnected. There are no parts of the story or world that feel out-of-place. Random NPCs you encounter in Act 1 will often reappear later in the game (unless you kill them for fun).
|
||||
|
||||
Having a game with such freedom of choice with a better, more cohesive story than most TV series and movies is nothing short of amazing.
|
||||
|
||||
## Character development
|
||||
|
||||
The character development in BG3 is phenomenal, for companions as well as NPCs, both with phenomenal voice acting. Even side characters you meet when making certain choices in Act 1 will show up in later acts, and have gone through some development. The development of the dream visitor is amazing as well.
|
||||
|
||||
The villains in the game are so incredibly well-written. The villains are not just your everyday antagonists, they have complex personalities, there is character development for them during the game, tons of backstory and with some you can even work together if you like.
|
||||
|
||||
And that is such an amazing part of the game to me as well: there is such a degree of freedom to morality. Without getting too much into the political influence of modern games, I think BG3 does an amazing job of offering choices without forcing anything upon the player, which I think is the best approach, regardless of what your personal preferences are.
|
||||
|
||||
To expand a bit upon the complex personality of NPCs, one thing I dislike in some games is when all characters are super obviously 'good' or 'bad'. This is definitely not the case in BG3. Yes, there are some 'evil' characters, but even a literal devil in the game is presented in an almost 'good' way in some parts. This moral ambiguity adds another amazing layer to the story.
|
||||
|
||||
The companions deserve mentioning here too, they all have their own storyline with tons of choices, will (dis)approve of your choices and their personalities can change drastically based on decisions made. A part that I have not played myself, but only seen in playthrough videos is the fact that for many characters, you can play with them as the main character, unlocking more unique content.
|
||||
|
||||
## Attention to detail
|
||||
|
||||
The attention to detail in BG3 is absolutely insane, which shows in many ways. A great example is that there are some voice lines that you will only hear in a modded playthrough, as your character enters a scene in a state that isn't possible in the vanilla game. Even in the vanilla game there are tons of hidden details that most players will miss.
|
||||
|
||||
Larian added some details in the game that they knew less than one percent of players will ever see, some might still be undiscovered. This is such a stark contrast to some released games that feel unpolished, or some even unfinished, until a few months after release.
|
||||
|
||||
A game like BG3 obviously needs a lot of failsafes, think of cases where crucial NPCs get killed, locking you out of progressing a certain quest. The failsafes in BG3 are very subtle, they don't limit your freedom to kill certain NPCs and even the failsafes have failsafes. I have had a few nights where I binge-watched videos about the failsafes in BG3 and it's still incredible to me how many edge cases were covered.
|
||||
|
||||
The attention to detail is also clear with NPCs in the game. Nearly all NPCs have voice lines, most you can talk to, and quite a few completely random NPCs will offer some sort of side quest (that of course fit into the rest of the story really well). The banter between companions is another great touch as well.
|
||||
|
||||
## Post-launch patches
|
||||
|
||||
To me, there is a very big difference between post-launch patches to make the game playable versus patches that improve the game. I won't be naming any games, but there are some prominent examples of games that were released and needed a few patches (not counting the day-one patch) just to be playable. BG3 was not one of them.
|
||||
|
||||
Larian did release patches after launch, but these were mostly to improve the game further and take care of community feedback. Some amazing features have been added here, like Honor Mode and the epilogue scenes. In addition, some voice lines have been replaced, they were already fine, but the fact Larian took the time to improve further on this to me is great.
|
||||
|
||||
## Mod support
|
||||
|
||||
Unofficial mod support was already available, but in a post-release patch, Larian added native mod support. Right now, it's even possible to have cross-platform multiplayer sessions with mods enabled (of course with some platform restrictions). The mods add a ton of new content and possibilities, adding even more replayability to the game.
|
||||
|
||||
## Music
|
||||
|
||||
I'm a huge fan of the soundtrack of BG3. The music fits the game incredibly well. The song of the boss fight with one of my favourite characters of the game, Raphael, still gives me goosebumps when I hear it. Then there's also the fact that this character stars in his own boss song, which is amazing to me as well.
|
||||
|
||||
## Lack of microtransactions
|
||||
|
||||
One of the things BG3 does that I hope will be adopted by other games is the lack of microtransactions. There are no DLCs and no special currencies purchased with real money. You buy the game and that's it. All post-release content was made available to all players.
|
||||
|
||||
The lack of microtransactions is another way to see that BG3 was made to be an amazing game, not as a corporate cash-grab. I'm not saying BG3 could never be made with investors being involved, but the freedom Larian had regarding monetization by not having to report to investors cannot be ignored.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Baldur's Gate 3 has set a new gold standard for RPGs. In my opinion, this is, so far, the closest we have ever gotten to a 'perfect' RPG. Whether you tend to play casual or are into min-maxing your builds at the highest difficulty, BG3 is a game worth playing, over and over.
|
||||
|
||||
BG3 sets an example that I hope many games - both RPGs and other genres - will follow. There are many conclusions you can draw from BG3's success about the games industry as a whole.
|
||||
|
||||
To me, it will be a game that I will keep playing for a long time. With no DnD experience there is a ton for me to discover in the base game already, and after that there will be tons more with the official and unofficial mods.
|
||||
@@ -1,107 +0,0 @@
|
||||
+++
|
||||
date = "2025-02-24"
|
||||
title = "Announcing Capsa, an Unreal Engine logging solution"
|
||||
slug = "announcing-capsa"
|
||||
tags = ["Capsa", "Tools", "Unreal Engine"]
|
||||
categories = ["Tools", "Games"]
|
||||
[params]
|
||||
metadescription = "Capsa makes Unreal Engine logging a lot better, especially for those working on multiplayer"
|
||||
metakeywords = 'capsa, unreal engine, logging'
|
||||
+++
|
||||
|
||||
_tl;dr: Capsa makes Unreal Engine logging a lot better, especially for those working on multiplayer. Check it out [here](https://capsa.gg/blog/announcing-capsa)._
|
||||
|
||||
I'm very excited to share what I've been working on over the last few months by announcing the v0.1 release of Capsa.
|
||||
|
||||
## Introducing Capsa
|
||||
|
||||
_An open-source Unreal Engine logging solution._
|
||||
|
||||
Ever since I started working on multiplayer games, I was quite surprised how few tools were available for working effectively with Unreal Engine logs for multiplayer games. After working on various titles, Mark Jawdoszak and I started building a tool called Capsa to address this. We've made this fully open-source so it can be used by other teams facing similar challenges.
|
||||
|
||||
More in-depth reasons why we've built Capsa and licensing can be found in [this blog post on Capsa.gg](https://capsa.gg/blog/why-we-built-capsa/).
|
||||
|
||||
## About Capsa
|
||||
|
||||
Capsa makes this easy by automatically collecting logs and making them accessible in a structured, searchable, and shareable way.
|
||||
|
||||
Some of the features Capsa offers:
|
||||
|
||||
- Logs with syntax highlighting and sharable links
|
||||
- Filter, search, and link logs
|
||||
- Merged client-server logs
|
||||
- Minimal setup, no game core or engine changes
|
||||
- Cloud-native, so host anywhere
|
||||
- Optimized for performance
|
||||
- Open-source, web stack AGPL3.0 and UE plugin MIT
|
||||
- Dark-mode support
|
||||
|
||||
Working on Capsa has been a blast and we've got tons of ideas of new features we want to add over the next few months.
|
||||
|
||||
To read the full announcement post, check out [Announcing Capsa](https://capsa.gg/blog/announcing-capsa).
|
||||
|
||||
## Building Capsa
|
||||
|
||||
Some notes on building Capsa and the tech we have used.
|
||||
|
||||
#### Extending Unreal Engine
|
||||
|
||||
We chose to extend Unreal Engine rather than replace functionality. We are hooking into `GLog` as an output device, so we can capture `UE_LOG` calls and we don't require game code changes with a new log system. We want to have a system that is easy to add to and easy to remove from games, and I think this is a great way to achieve that.
|
||||
|
||||
The received log lines are formatted according to our [Capsa log format](https://capsa.gg/docs/technical/protocol) so the server can parse it correctly. This way we don't change any default Unreal Engine log output, but we receive the correct format on the server
|
||||
|
||||
#### Golang server log parsing
|
||||
|
||||
The log is processed in O(1) complexity, reading one character at a time. A buffer is built with all line characters until encountering a `\n`. The line buffer is then used to extract metadata for that specific line. The metadata is stored in the database and the log chunks are stored in S3-compatible storage.
|
||||
|
||||
Due to the per-character algorithm for log chunk processing, it allows the server to be very performant with extracting the metadata. At the time of writing this, parsing a 100k line log chunk (which is much bigger than will be done in production) takes just around 60ms.
|
||||
|
||||
More details on log processing can be found on [Log processing](https://capsa.gg/docs/technical/log-processing).
|
||||
|
||||
#### Server-side merging and filtering
|
||||
|
||||
We had to make an important decision on doing log merging and filtering, whether we wanted to do this on the server or client. There are pros and cons for each, but we decided to do this server-side.
|
||||
|
||||
By doing this, we can re-use the existing log parsing code for incoming logs, get better performance, but it means that changing the filtering/merging settings requires reloading the full log from the server. We figured we needed server-side logic anyway, as large logs cannot be kept fully in the browser's memory, so at least for v0.1, all of the merging and filtering logic is handled server-side.
|
||||
|
||||
Because we store per-chunk metadata, we can ignore chunks that don't fit the filtering criteria as well, giving us a good performance boost. The benchmark for filtering lines from a 100.000-line chunk for streaming to a user (as is used for the admin panel) is around 70ms.
|
||||
|
||||
#### Golang server SQL
|
||||
|
||||
For handling database logic, I chose to go with sqlc and golang-migrate, as I have written in my [Golang SQL handling article](./best-golang-sql-handling). This has been a good decision so far, as I have full control over the SQL to optimize for performance. I have already done a few optimizations in the queries, which would not have been possible with ORMs.
|
||||
|
||||
#### JWT with private/public keys
|
||||
|
||||
Authentication is hand-rolled in the API server, so there is no reliance on external parties for this. JWTs are used for this. Instead of using a signing secret, a private/public keypair is used.
|
||||
|
||||
This allows the web panel to check the JWTs (stored as a cookie) if they are valid and have users reauthenticate if they are not.
|
||||
|
||||
All API requests are done from the browser directly to the API server to not unnecessarily increase server load for the web panel. Yet we still want to validate the JWTs on page load to make sure we don't perform API requests that will return in a `401` or `403` response.
|
||||
|
||||
#### Building Capsa cloud-native
|
||||
|
||||
Another concern when building Capsa was making it possible to deploy anywhere. This is why I have chosen to build the web stack with Golang and NextJS. Deploying these to any Linux or Windows system is very trivial. Both of the applications are available as Docker images and can be hosted anywhere by setting some environment variables.
|
||||
|
||||
#### Log processing web worker
|
||||
|
||||
The web panel uses a web worker to process incoming logs, to not block the main thread, which would make the website feel a lot slower due. This is achieved by writing a class to manage the web worker, which is then exposed in a React hook to work with the rest of the logic.
|
||||
|
||||
#### Runtime JSON validation
|
||||
|
||||
For the web panel, we have added runtime type validation by using Zod. The React code uses `useSWR` to call API endpoints as hooks. Instead of using API endpoints directly, we have added wrappers for requests, for example `const { trigger, isMutating } = useAddTitle();` to add a new game title. This hook internally calls the API endpoint with `useSWR`, validates the JSON it gets back with Zod, and sets an error if it's not valid.
|
||||
|
||||
By doing this, we can avoid runtime type exceptions.
|
||||
|
||||
## Continuing development
|
||||
|
||||
We have tons of ideas on how to improve or extend the current functionality or new features we can add. But before we add this, we want to make sure we are on the right path with Capsa.
|
||||
|
||||
Now v0.1 is released, we will be integrating this into a few Companion Group projects and gather feedback from there, as well as early-adopters. We will continue to fix bugs and work on existing and new features over time, to slowly work towards a v1.0 release.
|
||||
|
||||
For v1.0 we have a few feature ideas that would make Capsa even more powerful to improve the lives of developers working on Unreal Engine multiplayer games. We are working on a few proof-of-concepts for this, though these will take some time as we need to figure out details, as we don't want Capsa to take over game codebases, we want to keep Capsa non-intrusive.
|
||||
|
||||
## Want to become an early adopter?
|
||||
|
||||
If you are interested in becoming an early adopter, the details on getting started are in [Announcing Capsa](https://capsa.gg/blog/announcing-capsa). You can also reach out to me if you need any help, I'm more than happy to help you get started with Capsa.
|
||||
|
||||
All feedback - positive or negative - is very much welcome as well!
|
||||
@@ -1,116 +0,0 @@
|
||||
+++
|
||||
date = "2025-04-01"
|
||||
title = "Why I stopped using AI code editors"
|
||||
slug = "why-i-stopped-using-ai-code-editors"
|
||||
tags = ["AI"]
|
||||
categories = ["Tools"]
|
||||
[params]
|
||||
metadescription = "In the past I used AI code editors for all of my programming, but I stopped using it and recommend others to consider this as well"
|
||||
metakeywords = 'ai, code editors, llm, vibe coding'
|
||||
+++
|
||||
|
||||
_TL;DR: I chose to make using AI a manual action, because I felt the slow loss of competence over time when I relied on it, and I recommend everyone to be cautious with making AI a key part of their workflow._
|
||||
|
||||
In late 2022, I used AI tools for the first time, even before the first version of ChatGPT. In 2023, I started using AI-based tools in my development workflow. Initially, I was super impressed with the capabilities of these LLMs. The fact that I could just copy and paste obscure compiler errors along with the C++ source code, and be told where the error is caused felt like magic.
|
||||
|
||||
Once GitHub Copilot started becoming more and more powerful, I started using it more and more. I used various other LLM integrations right in my editor. Using AI was part of my workflow.
|
||||
|
||||
In late 2024 I removed all LLM integrations from my code editors. I still use LLMs occasionally and I do think AI can be used in a way that is very beneficial for many programmers. So then why don't I use AI-powered code editing tools?
|
||||
|
||||
## Tesla FSD
|
||||
|
||||
From 2019 to 2021 I drove a Tesla. Though I would never make the same purchase again, not for political reasons, just because the cars are quite low quality, very overpriced and a hell to repair or maintain.
|
||||
|
||||
When I got my Tesla, I started using the Full Self-Driving (FSD) anytime I could. It felt great to just put the car on FSD on the highway and zone out a bit. Switching lanes was as simple as hitting the turn signal, and the car would switch lanes. Driving for me was just getting to the highway, turning on FSD, telling the car to switch lanes every now and then, and listen to music/podcasts while zoning out.
|
||||
|
||||
If you drive a car often, you'll know that when you're driving on the highway, everything sort of happens automatically. Keeping your car in the lane at the right speed becomes a passive action, it does not require the type of focus that for example reading a book requires, it's the type of focus that walking requires, it happens in the background of your mind.
|
||||
|
||||
In the period from 2019 to 2021 I exclusively drove my Tesla for longer rides. After 2021, I went back to driving regular cars and making this switch was definitely not what I expected. Driving on the highway required my full attention for the first month or so, I had to re-learn keeping the car in the middle of the lane without thinking about it.
|
||||
|
||||
Being reliant on Tesla's FSD took away my own ability to go into autopilot.
|
||||
|
||||
## My experience with AI code editors
|
||||
|
||||
Working with AI-powered code editors was somewhat similar. Initially, I felt that I completed work a lot faster when assisted by AI. The work I was doing most of the time was not super complex, and AI felt like putting my Tesla on FSD, I could just guide the machine to do my work for me.
|
||||
|
||||
In my free time, I started working on a side project on my personal account on my work device. On this account, I did not have access to Copilot and my other cool, fancy AI tools. This is when using AI started to feel very similar to my Tesla FSD story.
|
||||
|
||||
I felt less competent at doing what was quite basic software development than a year or so before. All of a sudden, it made it very clear to me how reliant I had become on AI tools. Anytime I defined a function, I paused in my editor to wait until the AI tools would write the implementation for me. It took some effort to remember what the syntax was to write unit tests by hand.
|
||||
|
||||
With my work, AI started to become less useful over time as well. Not only did it take out the fun for me, but I started to feel a bit insecure about making some implementation decisions myself. Outsourcing the decisions to the AI seemed a lot easier. But sometimes, the AI couldn't figure things out, even with the best prompts. It was quite clear that because I did not practice the basics often, I was less capable with the harder parts as well.
|
||||
|
||||
## The loss of Fingerspitzengefühl
|
||||
|
||||
> **_Fingerspitzengefühl_** \[ˈfɪŋɐˌʃpɪtsənɡəˌfyːl\] is a German term, literally meaning _"finger tips feeling"_ and meaning intuitive flair or instinct, which has been adopted by the English language as a loanword. It describes a great situational awareness, and the ability to respond most appropriately and tactfully. [^fsg]
|
||||
|
||||
[^fsg]: Source: [Wikipedia](https://en.wikipedia.org/wiki/Fingerspitzengef%C3%BChl)
|
||||
|
||||
Defining seniority is a very tough thing. Though in my opinion a lot of being a "senior" is in soft-skills, when it comes to the technical hard-skills, a lot comes down to Fingerspitzengefühl. The longer you work with a language, framework or codebase, the more you develop this kind of intuition of what the correct approach is. The gut feeling of "something feels off" slowly turns into a feeling of "this is what we should do".
|
||||
|
||||
This developed intuition is not just on an architectural level. A big component is in the lower level details, when to use pointers (or what type of pointers), whether to use asserts or checks, what to pick from the standard library when multiple options are available (though senior C++ programmers still can't seem to agree on this).
|
||||
|
||||
This intuition is what I was slowly losing when relying on AI tools a lot. And this is coming from a lead developer. When I see a lot of hype about vibe coding, I can't help but think: how do you exactly expect to vibe code your way to senior? Where will you get the skills from to maintain and extend the vibe-coded codebase when the AI tools are down, or have become too expensive?
|
||||
|
||||
Even with larger context windows, more computing power, reasoning models or agents, there will be things that AI won't be able to do. Over time, the AI tools will be more and more powerful, sure. But when you receive a Slack message that "the website works fine, but the app is down in production; I tried it locally and there it works fine, nothing in Sentry either", good luck getting an AI agent to fix this for you. Maybe it can, maybe it can't. And when an AI agent can't figure it out, will your reply be "sorry, Cursor doesn't get it, will prompt more tomorrow"?
|
||||
|
||||
## You can get by without these tools
|
||||
|
||||
Sometimes it feels like you have to use AI or be out of a job in 6 months. We've been hearing the "3-6 months from now"-story for over two years at this point. I stopped trusting CEO promises about functionality "3-6 months from now" years ago. When I got my Tesla in 2019, I paid €6400 for functionality that was supposed to arrive in "3-6 months from now", and the functionality is still not present the way it was promised over 5 years ago.
|
||||
|
||||
Right now, it is unlikely that letting AI do your coding will work for projects larger than a university project. When working on legacy systems or larger projects in enterprises or when you need to work with and consult a lot of dependency internals (like I do with Unreal Engine), AI tools will often not be able to make things work. When you need to work with internal DSLs, tools or frameworks, good luck getting LLMs to generate useful output. For some industries, you can't even use AI tools at all for a multitude of reasons.
|
||||
|
||||
For some things you really should not _want_ to rely on AI. When implementing authentication systems like JWT[^jwt] signing or RBAC[^rbac], adding "and it should be secure" to the prompt won't make it secure if it's been trained on GitHub code that had CVEs[^cve]. When it comes to security, you should be the person who is responsible and understands this fully. Critical systems should be written and reviewed by humans, if we are heading to a situation where one AI agent writes the code, another reviews the autogenerated PR and then another AI agent deploys the code, we will see a huge spike of security issues soon.
|
||||
|
||||
[^jwt]: JSON Web Tokens, or JWTs are a common way to generate authentication tokens, among other uses
|
||||
|
||||
[^rbac]: Role-based access control (RBAC) is a mechanism to restrict system access by setting permissions and privileges
|
||||
|
||||
[^cve]: Common Vulnerabilities and Exposures (CVE) is a program used to identify, define and catalog publicly disclosed cybersecurity vulnerabilities, [cve.org](https://www.cve.org/)
|
||||
|
||||
## Where I draw the line
|
||||
|
||||
I still use AI, sometimes. I think it can be a great tool, when used wisely. I draw the line at integration. I keep AI fully separate from my code editor. All of the context, I add manually. I intentionally keep the effort required quite high, so it disincentivizes me.
|
||||
|
||||
Examples where I use AI for work include "convert these Go tests in structs to tests in a map", "convert this calculation to SIMD", or "when the content type is application/zlib, decode the body"[^aiworkexamples]. I have set up some custom instructions to only give me the code that has changed, and give me instructions for adding it. This way, I am still the one making the changes in the codebase. Just approving a Git diff is not enough, I want to manually add the code myself, only then do I feel confident to sign off on it and take responsibility for it.
|
||||
|
||||
[^aiworkexamples]: The specific contents here don't matter that much, they are just examples of what I use AI for
|
||||
|
||||
Another great use case for AI is learning. I often have questions that are quite uncommon, as I have a few very niche interests. Turns out, adding netcode to a custom game engine using ECS doesn't have a lot of learning resources. What has worked for me, is asking AI to explain pieces of code, like "explain this assembly code", "explain what this shader does", "which books go in-depth about resolving client/server desyncs in game engines". The AI seems to struggle with these sometimes, I'm getting mixed results, but the results are still much better than search engines. I will even use it for this article, though not for writing content, but for checking[^checkprompt].
|
||||
|
||||
[^checkprompt]: The prompt I've used for this article: "I want you to proofread an article I have written. I want you to give me feedback on incorrect grammar or broken sentences, using UK grammar. Do not comment on sentences that should be broken up or things that could be improved just slightly, only real errors. Do not return modified sentences, but point out where the issue is, under which paragraph, in which sentence and what the mistake is. I will make the required changes myself". It came back with a few typos, like "form" that should be "from", "eb" that should be "be".
|
||||
|
||||
Another benefit of using AI this way is the cost. No unnecessary API calls, manually managed contexts and more control over the LLM settings. I use a desktop application with a bunch of different LLMs hooked up to it. I have used it daily for the last 3 months or so, and in total, I have consumed around $4 in credits.
|
||||
|
||||
I do want to add that with some things I am more strict. On my personal website, I don't want any AI-generated content, whether that's text or images. I don't like AI generated images or 'art' personally for various reasons and I think AI-generated text lacks character, it feels very flat and boring. When something is created by humans, it to me has more value than when it's created by AI.
|
||||
|
||||
## Doing what you love
|
||||
|
||||
It is also worth noting that there are more things to think about than efficiency and productivity. It's also about doing what you love. If you love coding, keep doing it yourself, even if a computer might be better at it.
|
||||
|
||||
In 1997, Deep Blue won the chess match against the then world chess champion Garry Kasparov[^deepblue], yet people still play chess. When it comes to programming, I'd say that I program for the same reason that people still play chess[^chessclip]. Though chess and software development are very different, with chess being much more limited in scope, I think it is good to keep in mind that sometimes, we can do things just to enjoy them.
|
||||
|
||||
[^deepblue]: Source: [IBM History](https://www.ibm.com/history/deep-blue)
|
||||
|
||||
[^chessclip]: Source: [Tsoding on YouTube](https://youtu.be/-eS5-kaTSD0?si=Mf3ySN8QbpWhgLgK&t=328)
|
||||
|
||||
## My advice to new programmers
|
||||
|
||||
Don't become a forever junior who lets AI do all their work. If you want to become a programmer, learn to program yourself. Be curious, put in the time and effort to learn how things really work, and how things work in the layer below that. It really pays off. Learning how everything works under the hood and using that is amazing, just keep learning, don't be a prompt engineer (if you can even call that engineering). Believe me, it's more fun to be competent[^funcompetent].
|
||||
|
||||
[^funcompetent]: Source: [DHH in an interview on YouTube](https://youtu.be/mTa2d3OLXhg?si=vfjLD1DeoPZMERxA&t=1126)
|
||||
|
||||
Even though AI might be smarter than you, never blindly trust the AI output. Don't build your whole workflow around it. Sometimes try to work without it for a few days. The better at programming you are, the more AI will get in your way for the more complex work.
|
||||
|
||||
If you learn to code now, keep building your skills instead of letting AI do all the heavy lifting, you'll be capable of fixing the messes that vibe coding is now creating. I don't want to sound elitist, but if you don't want to learn to go beyond vibe coding, maybe coding isn't for you. Because positions where all work can be done by vibe coding are the ones that will be eliminated first when AI becomes more powerful.
|
||||
|
||||
And remember: if you cannot code without AI, you cannot code.
|
||||
|
||||
## Conclusion
|
||||
|
||||
When you are using AI, you are sacrificing knowledge for speed. Sometimes it's worth making this trade-off. Though it is important to remember that even the best athletes in the world are still doing their basic drills for a reason. The same applies to software development: you need to practice the basics, to be able to do the advanced work. You need to keep your axe sharp.
|
||||
|
||||
We are still a long way out from AI taking over our jobs. A lot of companies are creating FOMO[^fomo] as a sales tactic to get more customers, to show traction to their investors, to get another round of funding, to generate the next model that will definitely revolutionize everything.
|
||||
|
||||
[^fomo]: Fear of missing out
|
||||
|
||||
AI is a tool, it is not good or bad in itself, it's what you do with it. I do think it can be a great tool, as long as you are not reliant on it for your workflow. Make sure you can still work effectively without it, make sure you don't push code to production that you don't fully understand and don't think of AI as a replacement for your own thinking. Stay curious, keep learning.
|
||||
@@ -1,625 +0,0 @@
|
||||
+++
|
||||
date = "2025-04-16"
|
||||
title = "Creating and validating JWTs with public/private keys (JSON Web Key Set, JWKS)"
|
||||
slug = "creating-jwt-with-public-private-keys-jwk"
|
||||
tags = ["JWT", "Security", "Encryption", "Golang", "Typescript"]
|
||||
categories = ["Backend"]
|
||||
[params]
|
||||
metadescription = "How to securely sign and validate JWTs using public/private key pairs and leverage JSON Web Key Set (JWKS)"
|
||||
metakeywords = 'jwt, jwk, jwks, public private key, authentication, jose, token validation'
|
||||
+++
|
||||
|
||||
_This article discusses an implementation of JWT generation in Go, and validation in Go and Typescript. The library used for this, jose, has implementations for many other programming languages, so the principles can be applied in nearly all commonly used programming languages._
|
||||
|
||||
JSON Web Token[^jwtrfc] (JWT for short) is a very commonly used way to create and validate authentication tokens on the web. This article is not a tutorial of JWT basics, there are many resources available for that online already.
|
||||
|
||||
Most often, when generating JWTs, a secret value is used to create the third part of the JWT, the signature. This approach is great when there is a single service that generates and validates tokens. In some cases though, you want to sign a token on one service, and have it validated by another service. This could be achieved by sharing the signing secret, but this is not an optimal solution in terms of security, you would rather sign a JWT on one service and then validate it on another service, in a way that doesn't require you to have access to the secret value.
|
||||
|
||||
This can be achieved by using private/public key encryption. You can sign a JWT with a private key, and in the header include the link to where the public key can be found to validate the signature. This will allow other services to validate a JWT, without requiring access to the actual secret value to sign a key. To make this more concrete: this system is used by for example Auth0[^authzero] to sign keys to then allow you to validate these tokens without needing access to the signing secret. This system is also used by AWS Cognito[^awscognito].
|
||||
|
||||
I recently integrated this into [Capsa.gg](https://capsa.gg). The web panel has middleware to validate JWTs, which will redirect a user to log in if the key is invalid. When I started implementing this, I was not able to find a lot of resources on this unfortunately and it took quite some trial-and-error to complete. Yet in the end, implementing JWTs with private key signing and public key validation was quite straight-forward. Hence why I'm sharing my findings here, in the hope it will be useful to others.
|
||||
|
||||
This article will implement JWT generation with a private key and gives an example of validating with the private key. Some code is omitted, a working example project can be found on Github: [lucianonooijen/jwt-public-private-key-demo](https://github.com/lucianonooijen/jwt-public-private-key-demo). This also contains some unit tests for validation, which are not included in this article.
|
||||
|
||||
> [!CAUTION]+ Security disclaimer
|
||||
> Please don't follow this guide as if it's gospel, make sure you know what you are doing. This article and the example project are meant as starting points, to be adopted according to specific needs. Never blindly trust code on the internet, especially when it comes to security. The footnotes of this article contain trusted sources that can be used for further reading.
|
||||
|
||||
[^jwtrfc]: [RFC7519](https://datatracker.ietf.org/doc/html/rfc7519)
|
||||
|
||||
[^authzero]: [Auth0 JSON Web Key Sets](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets)
|
||||
|
||||
[^awscognito]: [AWS Cognito Token Validation](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-aws-jwt-verify)
|
||||
|
||||
## JSON Web Key
|
||||
|
||||
When generating a JWT, the first part of the token, the header, contains information about how the key is signed. The most important field for our use-case, is the algorithm field `alg`.
|
||||
|
||||
For example, when signing a token with just a simple secret, the header contents are
|
||||
|
||||
```json
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
indicating that the algorithm used is "HMAC using SHA-256"[^hs256]. This is the easiest way to sign and verify JWTs, used in many projects. You need a secret to sign the token, and use the same token to validate it.
|
||||
|
||||
Now let's look at the header of a JWT that has been signed with the public/private key setup that this article discusses:
|
||||
|
||||
```json
|
||||
{
|
||||
"alg": "RS256",
|
||||
"jku": "http://localhost:4000/.well-known/jwks.json",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
The algorithm field here now has a different value, `RS256`, indicating the digital signature algorithm used is "RSASSA-PKCS1-v1_5 using SHA-256"[^rs256].
|
||||
|
||||
This looks like a cat walked across the keyboard, but we can break this down. `RSA` indicates that we are using RSA, so a public/private key setup, `SSA` indicates "With Appendix", meaning the signature is separate from the message, which makes sense with how JWTs work. `PKCS1` refers to the "Public-Key Cryptography Standards #1"[^pkcsone], which defines how RSA should be used. `v1_5` indicates the padding scheme, which is older, but used and supported in JWTs.
|
||||
|
||||
A new field in the header, is the `jku` field. This is the JWK Set URL[^jku], often being `<service_url>/.well-known/jwks.json`[^jwksuri]. The URL here should resolve to the public key data when making a GET request to this URL over TLS[^jku-optional]. The value of this field should not be blindly trusted at runtime unless it matches an expected value, or there is the risk of a key injection attack. When fetching this URL, there is a specific format how the data should be encoded[^jwkformat], which is called the JSON Web Key Set, or JWKS. "Set" here meaning a collection of multiple JSON Web Keys (singular: JWK). We will discuss this a bit more later. The important takeaway here is: this URL contains the public key data of all signing keys that should be considered valid.
|
||||
|
||||
A small note to add: for the example project, we are not fully following the RFC, as we fetch the JWKS without TLS. When implementing a JWKS endpoint, it should always be to over TLS to follow the RFC[^jku].
|
||||
|
||||
To summarize: when implementing public/private key signing for JWTs, the JWT should contain the correct algorithm, as well as a URL where valid keys can be retrieved for validating the tokens. There is a specific format on how the public keys must be made available.
|
||||
|
||||
<!--
|
||||
mmdc -i mermaid.mmd -o static/img/0011-1.png -b transparent
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
|
||||
participant A as Auth
|
||||
participant S as Service
|
||||
participant U as User
|
||||
|
||||
par Authentication
|
||||
U->>A: Request JWT via login
|
||||
A->>U: Sends JWT, signed with private key
|
||||
end
|
||||
|
||||
par AuthenticatedRequest
|
||||
U->>S: Sends request with JWT
|
||||
opt Fetch JWKS
|
||||
S->>A: Request JWKS
|
||||
A->>S: Send JWKS
|
||||
end
|
||||
S- ->S: Validates JWT validity using JWKS || TODO: REMOVE SPACE
|
||||
S->>U: Sends response based on JWT validity
|
||||
end
|
||||
```
|
||||
-->
|
||||
|
||||

|
||||
|
||||
[^pkcsone]: [RFC8017](https://datatracker.ietf.org/doc/html/rfc8017)
|
||||
|
||||
[^jwkformat]: [RFC7517: JSON Web Key (JWK) Format](https://datatracker.ietf.org/doc/html/rfc7517#section-4)
|
||||
|
||||
[^hs256]: [RFC7518: HMAC with SHA-2 Functions](https://datatracker.ietf.org/doc/html/rfc7518#section-3.2)
|
||||
|
||||
[^rs256]: [RFC7519: Digital Signature with RSASSA-PKCS1-v1_5](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1)
|
||||
|
||||
[^jku]: [RFC7515: "jku" (JWK Set URL) Header Parameter](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.2)
|
||||
|
||||
[^jwksuri]: This is not a strict requirement for our use-case here, but comes from the conventions for OAuth2.0 Authorization Server Metadata. This is also the endpoint used by Auth0 and AWS Cognito. See [RFC8414: Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414#section-2) for more info.
|
||||
|
||||
[^jku-optional]: The RFC marks this field as optional but for our use-case we want to include this field.
|
||||
|
||||
## Public/private key generation
|
||||
|
||||
So, let's start implementing JWT signing with public/private keys. The first thing we need, is to actually generate the keys. We need to generate keys with at least 2048 bits[^keysize].
|
||||
|
||||
We don't want to generate a key, keep it in memory and use that right away. The keys should be reused if the application starts up. You can generate the key in any way you like, this is an example of how to do this in Go. Let's assume we want to create a private key, then extract the public key from that, encode that as strings that we will return to the calling function, which will write the keys to disk.
|
||||
|
||||
```go
|
||||
package token
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RsaKeySet is a set of generated public and private keys.
|
||||
type RsaKeySet struct {
|
||||
PrivateKey []byte
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
// GenerateRsaKeySet generates a private/public key set for signing JWKs.
|
||||
func GenerateRsaKeySet() (*RsaKeySet, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating private key: %w", err)
|
||||
}
|
||||
|
||||
return EncodePrivateKeyToBytes(privateKey)
|
||||
}
|
||||
|
||||
// EncodePrivateKeyToBytes takes in a *rsa.PrivateKey and encodes this to the private and public PEM key bytes.
|
||||
func EncodePrivateKeyToBytes(privateKey *rsa.PrivateKey) (*RsaKeySet, error) {
|
||||
privateKeyBytes := encodePrivateKeyToPEM(privateKey)
|
||||
|
||||
publicKeyBytes, err := encodePublicKeyToPem(privateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating public key: %w", err)
|
||||
}
|
||||
|
||||
keyData := RsaKeySet{
|
||||
PrivateKey: privateKeyBytes,
|
||||
PublicKey: publicKeyBytes,
|
||||
}
|
||||
|
||||
return &keyData, nil
|
||||
}
|
||||
|
||||
func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
|
||||
// Get PKCS #1, ASN.1 DER format
|
||||
privateKeyContents := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
|
||||
// pem.Block
|
||||
privateBlock := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: privateKeyContents,
|
||||
}
|
||||
|
||||
// Private key in PEM format
|
||||
privatePEM := pem.EncodeToMemory(&privateBlock)
|
||||
|
||||
return privatePEM
|
||||
}
|
||||
|
||||
func encodePublicKeyToPem(privateKey *rsa.PrivateKey) ([]byte, error) {
|
||||
// Extract the public key
|
||||
publicKey := &privateKey.PublicKey
|
||||
|
||||
// Marshal the public key to PKIX, ASN.1 DER form
|
||||
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling public key: %w", err)
|
||||
}
|
||||
|
||||
// Create the PEM block
|
||||
publicKeyPEMBlock := pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyBytes,
|
||||
}
|
||||
|
||||
// Encode the PEM block to []byte
|
||||
publicKeyPEM := pem.EncodeToMemory(&publicKeyPEMBlock)
|
||||
|
||||
return publicKeyPEM, nil
|
||||
}
|
||||
```
|
||||
|
||||
_Security disclaimer: keys must be handled with care. You need to use different keys for different environments and have proper practices for handling secrets. Please make sure you handle key generation and storage with great care._
|
||||
|
||||
[^keysize]: [RFC7518: Digital Signature with RSASSA-PKCS1-v1_5](https://datatracker.ietf.org/doc/html/rfc7518#section-3.3)
|
||||
|
||||
## Accessing the public and private key
|
||||
|
||||
So we have generated a private and public key and persisted the contents to disk. Let's now work on the actual JWT logic that uses these keys. In practice, we don't need to load the public key from disk, only the private key, which we will use to get the public key values.
|
||||
|
||||
So let's write two helper functions: one that will load the private key from disk, and another that will decode a base64 string that is passed into it, for example if the private key value is set as an environment variable, base64 encoded.
|
||||
|
||||
```go
|
||||
package token
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const blockTypeRsaPrivateKey = "RSA PRIVATE KEY"
|
||||
|
||||
var (
|
||||
// ErrorPemPrivateKeyDecoding indicates that the decoding of the private-key containing PEM block failed.
|
||||
ErrorPemPrivateKeyDecoding = errors.New("failed to decode PEM block containing private key")
|
||||
)
|
||||
|
||||
// LoadPrivateKeyFromPath reads the private key from a file and returns a rsa.PrivateKey struct.
|
||||
func LoadPrivateKeyFromPath(path string) (*rsa.PrivateKey, error) {
|
||||
// Read the file
|
||||
keyBytes, err := os.ReadFile(path) //nolint:gosec // "G304: Potential file inclusion via variable", this path is set in the config.yml and should never contain user input
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read private key file: %w", err)
|
||||
}
|
||||
|
||||
// Decode the PEM block
|
||||
block, _ := pem.Decode(keyBytes)
|
||||
if block == nil || block.Type != blockTypeRsaPrivateKey {
|
||||
return nil, ErrorPemPrivateKeyDecoding
|
||||
}
|
||||
|
||||
// Parse the private key
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// LoadPrivateKeyFromBase64String reads the private key from a base64 string and returns a rsa.PrivateKey struct.
|
||||
func LoadPrivateKeyFromBase64String(b64 string) (*rsa.PrivateKey, error) {
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode private key base64 string: %w", err)
|
||||
}
|
||||
|
||||
// Decode the PEM block
|
||||
block, _ := pem.Decode(keyBytes)
|
||||
if block == nil || block.Type != blockTypeRsaPrivateKey {
|
||||
return nil, ErrorPemPrivateKeyDecoding
|
||||
}
|
||||
|
||||
// Parse the private key
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
```
|
||||
|
||||
We now have our `*rsa.PrivateKey` instance again that we can use to sign JWTs with.
|
||||
|
||||
## Signing JWTs
|
||||
|
||||
To sign the JWTs, we will be using go-jose[^gojose]. Let's create a struct called `Token`, which holds the data we need, and exposes methods to sign and validate tokens:
|
||||
|
||||
```go
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
privateKey *rsa.PrivateKey
|
||||
jwk *jose.JSONWebKey
|
||||
signer *jose.Signer
|
||||
}
|
||||
```
|
||||
|
||||
With our `Token` struct defined, let's create a function called `New` in our package, that will accept a private key and returns an initialized `Token` instance:
|
||||
|
||||
```go
|
||||
const (
|
||||
keyID = "jwt-demo-server-jwk"
|
||||
algorithm = jose.RS256
|
||||
)
|
||||
|
||||
// New returns a Token instance after validating the *rsa.PrivateKey.
|
||||
func New(pk *rsa.PrivateKey) (*Token, error) {
|
||||
if pk == nil {
|
||||
return nil, errors.New("private key argument is required")
|
||||
}
|
||||
|
||||
if err := pk.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("error validating private key: %w", err)
|
||||
}
|
||||
|
||||
jwk := jose.JSONWebKey{
|
||||
Key: pk,
|
||||
Use: "sig",
|
||||
Algorithm: string(algorithm),
|
||||
KeyID: keyID,
|
||||
}
|
||||
|
||||
signerOptions := (&jose.SignerOptions{}).
|
||||
WithHeader("alg", algorithm).
|
||||
WithHeader("typ", "JWT").
|
||||
WithHeader("jku", "http://localhost:4000/.well-known/jwks.json")
|
||||
|
||||
jwtSigner, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: algorithm,
|
||||
Key: pk,
|
||||
}, signerOptions)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating jwt signer: %w", err)
|
||||
}
|
||||
|
||||
jwkInstance := Token{
|
||||
privateKey: pk,
|
||||
jwk: &jwk,
|
||||
signer: &jwtSigner,
|
||||
}
|
||||
|
||||
return &jwkInstance, nil
|
||||
}
|
||||
```
|
||||
|
||||
A few important notes here: the `keyID` is an identifier for which type of key we are dealing with. If you have two token types: a user and an admin, you want to use different key IDs, which other services should validate if it's the right `keyID`. The `jku` field is now hardcoded in this example, you will most likely need to add a method that uses the application config to generate the correct URL, which is using `https`, not `http` like in this example. In the `jose.JSONWebKey`, we set `Use: "sig"`, because we use this key for signing, not for encrypting the JWT[^useparam], that is beyond the scope of this article.
|
||||
|
||||
With our new `Token` instance, let's add a few methods that allows us to actually generate and validate JWTs. For our example application, we will only have a single token type. When dealing with multiple token types, you will most likely have to generate multiple functions for generation and validation, for each token type.
|
||||
|
||||
Let's generate a JWT:
|
||||
|
||||
```go
|
||||
const (
|
||||
Audience = "example"
|
||||
Issuer = "jwt-demo-server"
|
||||
)
|
||||
|
||||
|
||||
func (t *Token) GenerateJwt(subject, jwtId, name, role string) (string, error) {
|
||||
now := time.Now()
|
||||
expiryHours := time.Duration(48) * time.Hour
|
||||
|
||||
claims := JwtClaims{
|
||||
Issuer: Issuer,
|
||||
Subject: subject,
|
||||
Audience: Audience,
|
||||
Expiry: now.Add(expiryHours).Unix(),
|
||||
NotBefore: now.Unix(),
|
||||
IssuedAt: now.Unix(),
|
||||
JwtID: jwtId,
|
||||
Name: name,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
tok, err := t.generateTokenForClaims(claims)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error signing token: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("generated client jwt with subject: %s\n", subject)
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
// generateTokenForClaims is a function that will sign the JwtClaims passed in.
|
||||
// WARNING: This method should only be called in public wrapper functions and not exposed directly.
|
||||
// For generating tokens in production code, always use the audience-specific methods.
|
||||
func (t *Token) generateTokenForClaims(claims JwtClaims) (string, error) { //nolint:gocritic // Jose needs val, not ref
|
||||
return jwt.Signed(*t.signer).Claims(claims).Serialize()
|
||||
}
|
||||
```
|
||||
|
||||
The scary looking second function performs the logic to actually generate the JWT string with the claims passed in. Discussing the JWT claims is beyond the scope of this article, these are explained in the RFC[^jwtrfc]. An example can be found [in the example project](https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/server/internal/token/claims.go). The note "For generating tokens in production code, always use the audience-specific methods." refers to using wrapper functions to generate keys with the correct data.
|
||||
|
||||
[^gojose]: [go-jose/go-jose](https://github.com/go-jose/go-jose/)
|
||||
|
||||
[^useparam]: [RFC7517: "use" (Public Key Use) Parameter](https://datatracker.ietf.org/doc/html/rfc7517#section-4.2)
|
||||
|
||||
## Validating JWTs using the private key
|
||||
|
||||
Of course we also want to be able to validate the JWT on the service that generated the key. Let's add that before we implement the validation on an external service:
|
||||
|
||||
```go
|
||||
var (
|
||||
// ErrorJwtParsing indicates that the signature of the JWT is not valid.
|
||||
ErrorJwtParsing = errors.New("parsing JWT failed")
|
||||
|
||||
// ErrorJwtValidation indicates that the token claims could not be validated.
|
||||
ErrorJwtValidation = errors.New("claim validation for JWT failed")
|
||||
|
||||
// ErrorJwtConversion indicates that the token claims to struct conversion failed.
|
||||
ErrorJwtConversion = errors.New("claim conversion for JWT failed")
|
||||
)
|
||||
|
||||
func (t *Token) ValidateJwt(token string) (*JwtClaims, error) {
|
||||
parsedToken, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{algorithm})
|
||||
if err != nil {
|
||||
log.Printf("error parsing signed token: %s\n", err)
|
||||
|
||||
return nil, ErrorJwtParsing
|
||||
}
|
||||
|
||||
claims := Claims{}
|
||||
|
||||
// Note: dereference here is very important!
|
||||
// The jose code checks for *rsa.PublicKey specifically, and does not accept rsa.PublicKey
|
||||
err = parsedToken.Claims(&t.privateKey.PublicKey, &claims)
|
||||
if err != nil {
|
||||
log.Printf("error validating token claims: %s\n", err)
|
||||
|
||||
return nil, ErrorJwtValidation
|
||||
}
|
||||
|
||||
c, err := claims.jwtClaims()
|
||||
if err != nil {
|
||||
log.Printf("error converting token claims: %s\n", err)
|
||||
|
||||
return nil, ErrorJwtConversion
|
||||
}
|
||||
|
||||
err = validateJwtClaims(c)
|
||||
if err != nil {
|
||||
log.Printf("error validating token claims: %s\n", err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This code assumes a `type Claims map[string]any` definition. On this type, we add a method `jwtClaims` that converts this map to our own custom claims type, which was mentioned before. For inspiration, you can check [this example](https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/server/internal/token/claims.go). We also call our own `validateJwtClaims` method, which is beyond the scope of this article, but it validates a bunch of fields, you can draw inspiration from [this example](https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/server/internal/token/jwt_validate_claims.go). The validation required depends on your application. You should also validate that the algorithm and key id match the expected values, or reject the token if they don't.
|
||||
|
||||
## Sharing the public key
|
||||
|
||||
Remember that when signing tokens with RS256 (public/private key setup), that we should include a URL where the JWKS can be retrieved? Let's add that now.
|
||||
|
||||
In our `Token` instance, we want to add a method that allows us to retrieve the JSON representation of the public key. Getting the JSON representation of the public key is very straight-forward:
|
||||
|
||||
```go
|
||||
// GetPublicKey takes the public part of the private key and marshals this to JSON for the .well-known/jwks.json endpoint.
|
||||
func (t *Token) GetPublicKey() ([]byte, error) {
|
||||
// SECURITY NOTE: NEVER REMOVE `.Public()` FROM THIS CODE
|
||||
// Otherwise, the private key will be shared and the whole token system will be compromised!
|
||||
return t.jwk.Public().MarshalJSON()
|
||||
}
|
||||
```
|
||||
|
||||
**Make sure you use `jwk.Public()`!** If you do not include the `Public()` part in here, you will expose the full _private_ key, meaning the whole token system will be compromised!
|
||||
|
||||
We can add a simple handler to return this key like this, assuming we have our handlers in a `handlers` struct which has an instance of `Token` in the `token` struct field:
|
||||
|
||||
```go
|
||||
func (h *handlers) Jwk(c *gin.Context) {
|
||||
pubKey, err := h.token.GetPublicKey()
|
||||
if err != nil {
|
||||
// handle error
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("content-type", "application/json")
|
||||
|
||||
// Manually writing JSON for compatibility with the []byte from pubKey
|
||||
c.Writer.WriteString(`{"keys":[`) //nolint:errcheck,gosec // This is fine
|
||||
c.Writer.Write(pubKey) //nolint:errcheck,gosec // This is fine
|
||||
c.Writer.WriteString(`]}`) //nolint:errcheck,gosec // This is fine
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
```
|
||||
|
||||
The proper JSON marshalling is left as an exercise for the reader.
|
||||
|
||||
## Inspecting the JWT
|
||||
|
||||
Let's add a super simple API endpoint `GET /jwt` that generates a JWT with some hardcoded values. This of course is just for demo purposes and not secure to be used in production. In production, add authentication checks for users to log in and use the proper JWT claim values.
|
||||
|
||||
Example handler for testing:
|
||||
|
||||
```go
|
||||
func (h *handlers) GetJwt(c *gin.Context) {
|
||||
// Never do this in production, just for demo!
|
||||
jwt, err := h.token.GenerateJwt("42", "1337", "John Doe", "Example")
|
||||
if err != nil {
|
||||
// Handle error
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the token passes our own validation, plus get the claims so we can return the expiry time
|
||||
claims, err := h.token.ValidateJwt(jwt)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, struct {
|
||||
Token string `json:"token"`
|
||||
Expiry int64 `json:"expiry"`
|
||||
}{Token: jwt, Expiry: claims.Expiry}) // You should do this properly
|
||||
}
|
||||
```
|
||||
|
||||
So to test, let's run the server and run `curl http://localhost:4000/jwt | jq '.token'`[^jq] to generate a token, and parse the JWT string from the response. Let's copy the value into [jwt.io](https://jwt.io). We can see that the header values and claims are set correctly.
|
||||
|
||||
[^jq]: jq is a very helpful tool for working with JSON data on the command line, but how to use it are beyond the scope of this article.
|
||||
|
||||
## Validating the JWT in NextJS
|
||||
|
||||
So now we get to the exciting part, validating the JWT in a separate service. Let's initialize an empty NextJS project and add some JWT validation middleware:
|
||||
|
||||
middleware.ts:
|
||||
|
||||
```ts
|
||||
import {
|
||||
deleteJwtCookie,
|
||||
getJwtCookieFromRequest,
|
||||
} from "@/data/jwt/cookiesServer";
|
||||
import JwtValidator from "@/server/jwt";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
// This middleware checks if users are logged in.
|
||||
// This is not implemented for security reasons, as that is done on the server.
|
||||
// It simply serves as a way to make sure users don't get 4xx errors.
|
||||
export async function middleware(req: NextRequest) {
|
||||
const { pathname, search, origin, basePath } = req.nextUrl;
|
||||
const path = `${basePath}${pathname === "/" ? "" : pathname}${search}`;
|
||||
const logBase = `[middleware][${req.method} ${path}]:`;
|
||||
|
||||
const token = await getJwtCookieFromRequest(req);
|
||||
const isAuthenticated = token ? await JwtValidator.ValidateJwt(token) : false;
|
||||
const isAuthRoute = req.nextUrl.pathname.startsWith("/auth");
|
||||
|
||||
console.log(
|
||||
logBase,
|
||||
"received request, isAuthenticated",
|
||||
isAuthenticated,
|
||||
"| isAuthRoute",
|
||||
isAuthRoute,
|
||||
);
|
||||
|
||||
// Logged-in users accessing login routes should be redirected to the homepage
|
||||
if (isAuthenticated && isAuthRoute) {
|
||||
console.log(
|
||||
logBase,
|
||||
"redirecting logged in user from auth route to",
|
||||
req.url,
|
||||
);
|
||||
|
||||
return NextResponse.redirect(new URL("/", req.url));
|
||||
}
|
||||
|
||||
// Send users who are not logged in and request non-auth pages to the login page
|
||||
if (!isAuthenticated && !isAuthRoute) {
|
||||
await deleteJwtCookie(req);
|
||||
|
||||
const signInUrl = new URL(`${basePath}/auth/login`, origin);
|
||||
|
||||
if (path !== "") {
|
||||
signInUrl.searchParams.set("redirect", path);
|
||||
}
|
||||
|
||||
console.log(
|
||||
logBase,
|
||||
"redirecting non logged in user to",
|
||||
signInUrl.toString(),
|
||||
);
|
||||
|
||||
return NextResponse.redirect(signInUrl);
|
||||
}
|
||||
|
||||
console.log(logBase, "continue executing request");
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
|
||||
*/
|
||||
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
The actual `JwtValidator` implementation will be specific for each application and should take care to properly validate tokens. The implementation for the demo project can be found [here](https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/client/server/jwt.ts). For validation, you should think of checking the algorithm, expected audience, issuer and timestamps.
|
||||
|
||||
It is good practice to set the JWKS endpoint in your application config, so that you only fetch keys from that location and reject keys that have a different `jku` field in the header. Don't trust the `jku` field from the header and start fetching the key from there, in case a malicious actor set up their own service to sign keys. Security here is very important, so take good care to minimize the possible attack vectors.
|
||||
|
||||
## Seeing this in action
|
||||
|
||||
The example project for this article can be found [here](https://github.com/lucianonooijen/jwt-public-private-key-demo) on GitHub.
|
||||
|
||||
For a more complete example with different types of keys, you can take a look at [Capsa's token package](https://github.com/capsa-gg/capsa/tree/main/server/internal/infrastructure/token) and at [Capsa's NextJS middleware](https://github.com/capsa-gg/capsa/blob/main/web/middleware.ts).
|
||||
|
||||
> [!CAUTION]+ Disclaimer
|
||||
> Never blindly trust code on the internet, especially when it comes to security. Use this article as a starting point but please do your own research and familiarize yourself with the relevant RFCs when implementing this for a production application.
|
||||
|
||||
<br />
|
||||
@@ -15,4 +15,4 @@ Mail: [thomas.sindt@gmx.de](mailto:thomas.sindt@gmx.de)
|
||||
|
||||
WWW: [https://www.thomas-sindt.de](https://www.thomas-sindt.de)
|
||||
|
||||
LinkedIn: [https://www.linkedin.com/in/thomas-sindt](https://www.linkedin.com/in/thomas-sindt)
|
||||
LinkedIn: [https://www.linkedin.com/in/thomas-sindt-555820324](https://www.linkedin.com/in/thomas-sindt-555820324)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
+++
|
||||
title = "Fotografie"
|
||||
slug = "fotografie"
|
||||
+++
|
||||
|
||||
#### Einleitung
|
||||
Seit ca. 10 Jahren beschäftige ich mich jetzt mit Fotografie. Die Notwendigkeit wurde aus der Anforderung heraus, ein Plattencover zu gestallten, geboren. Seitdem bin ich mit dem Fotoaparat unterwegs.
|
||||
Neben Bildern der Familie versuche ich, Motive aus unbekannten Blickwinkeln aufzunehmen, einfach, die Welt anders zu sehen.
|
||||
|
||||
#### Beispiele
|
||||
|
||||
{{< image-gallery gallery_dir="/album" >}}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
+++
|
||||
title = "Learning"
|
||||
slug = "learning"
|
||||
+++
|
||||
|
||||
Date: July 18, 2020
|
||||
|
||||
_Below is quite a personal story about my particular past and experiences. I have decided to share this publicly, hoping that this might help others in some way._
|
||||
|
||||
## How I got started
|
||||
|
||||
It's not a secret that I don't have formal education in Computer Science, that I don't have any formal education after high-school at all. I thought this was _the_ way to go and University was just teaching old stuff you don't need (and how wrong I was, we'll come back to that in a bit). It was at this time I was involved into some cringy programmer stuff, like passionately discussing about tabs vs spaces (and dismissing Go because it enforces tabs) or arguing Java should never be used for greenfield projects. You can say this was my '[peak of mount stupid](https://wisdomofhands.blogspot.com/2015/12/the-peak-of-mount-stupid.html)'.
|
||||
|
||||
Learning through freelancing and figuring things out along the way had been my way to learn until around early 2017. I realized that my 'learn as you go, when you need it'-approach had reached it's limits and there were many things I did not know that I really should know, it like learning a new (human spoken) language by just learning the new words and not the grammar. After realizing this, I started reading a lot of books on different programming languages, styles, frameworks, software architecture, site reliability engineering and soft skills. This has brought me a long way.
|
||||
|
||||
## Hitting a roadblock
|
||||
|
||||
Around the beginning of 2020, I felt like I was hitting the limit of this approach, like I knew a lot, but I could not comprehend how everything fit together. Like I've learned to play many instruments from sheet music, but never learned the underlying principles, the music theory. I could use a lot of tools, but I could not make them myself. I realized there was a lot to learn, not to learn what to do, I knew that already, but _why_ it is done this way. I started digging into topics not directly related to what I had always been focussing on (web and mobile development), I read about compiler engineering, assembly languages, virtual machines and operating systems. I learned a ton (I'd say this is the point where I turned from a software _developer_ into a true software _engineer_). But still, there were pieces missing in the puzzle. To continue with the music metaphor, I understood how symphonies were written, but I could not write them myself from start to finish.
|
||||
|
||||
In May of 2020, during the COVID-19 pandemic, I finally had the time for self-reflection. I came to the realization that I was utterly and completely wrong about Universities teaching you only old stuff you don't need. I realized that you need the old stuff, like calculus and algebra, to understand (and build!) the new stuff, like artificial intelligence and embedded systems systems (yes, the systems used to build embedded systems). This is something many people pointed out to me, but I was too stubborn to accept earlier. I could write a simple domain-specific language parser, but I could not prove it to be working through mathematics. I knew what my code would do with system calls, but I didn't know _how_ the CPU actually performed the task at the metal-level. I knew how to establish and use TCP connections, but I could not explain how the payloads are verified. I missed the theoretical side of computer science.
|
||||
|
||||
## Enrolling in formal education?
|
||||
|
||||
During June of 2020, I seriously considered going through a three year Bachelor's degree program, but I unfortunately could not find a University that would be a great fit, considering I still have to work full-time at Bytecode and it was already June. The Open University curriculum did not offer enough of a challenge for me and the TU Delft (less than 5 minutes from both my home and Bytecode's office) requires full-time (and I had to apply before January).
|
||||
|
||||
Another issue was that there would be quite a lot of overlap between the some courses' content and what I already know (especially in the first and second year), not gaining (a lot of) new knowledge while taking quite some time to complete. The thing is, my focus wasn't on getting a degree, it was purely about the knowledge gained in the process. For me, the best solution would be to follow a few Bachelor's courses to fill some knowledge gaps and then follow a Master's track (not getting a degree, just following the lectures and excercises) to gain in-depth knowledge, but this would be very unlikely to happen, as Universities don't tend to be so flexible.
|
||||
|
||||
## Subjects for study
|
||||
|
||||
Another issue I faced during this time was that in The Netherlands, there is only a very small part for study in other fields. In the US, there is a concept called a Major, in The Netherlands, the Major will be your only focus, with a 6-month period for a Minor where you can choose what to do. During my self-reflection I noticed I was laser-focussed on software engineering for many years, neglecting other parts of study that would help me develop into a better human being and in turn helping me grow in my day-job too. I wrote down what areas interest me, what I'd like to learn more about and categorized them:
|
||||
|
||||
- Computer science
|
||||
- Fundamentals (maths and physics that allow computers to work on the metal-level, algorithms and data structures)
|
||||
- Theories (theory of computation, automata, complexity theory, computability)
|
||||
- Networking (how computers communicate on the lowest level, the ones and zeroes)
|
||||
- Computer graphics (how it's possible to display graphics onto a screen, AR/VR)
|
||||
- Compilers (the mathematical and scientific side of compilers)
|
||||
- Databases (not how to use them, but how to build them, how data is stored and retrieved)
|
||||
- Robotics (how embedded systems allow hardware to move, the electrical engineering behind it)
|
||||
- Computational Science (computer science, applied to solve large problems)
|
||||
- Cyber security fundamentals (not just how to use metasploit, but true cybersec fundamentals)
|
||||
- Linguistics
|
||||
- Fundamentals of linguistics
|
||||
- Latin
|
||||
- German
|
||||
- Italian
|
||||
- French
|
||||
- Koine Greek
|
||||
- Spanish
|
||||
- Hebrew
|
||||
- Arabic
|
||||
- Philosophy
|
||||
- Stoicism
|
||||
- Epicureanism
|
||||
- Platonism
|
||||
- Theology
|
||||
- Modern philosophy
|
||||
- History
|
||||
- Ancient Roman
|
||||
- Ancient Greek
|
||||
- Communist revolution
|
||||
- American history
|
||||
- Enlightenment
|
||||
- Economic and political theories
|
||||
- Libertarianism
|
||||
- Communism/socialism
|
||||
- Keynes
|
||||
- Anarchism
|
||||
- Music
|
||||
- Music theory
|
||||
- Classical music
|
||||
- Jazz
|
||||
- Sport
|
||||
- Chess
|
||||
- Golf
|
||||
- Tennis
|
||||
- Misc
|
||||
- Rhethoric
|
||||
- Speaking and presenting
|
||||
- Debating and negotiation
|
||||
- Meditation/mindfulness
|
||||
- Storytelling
|
||||
- Cooking different cuisines
|
||||
- Gardening
|
||||
|
||||
_Note: wanting to learn about a subject is not the same as believing in it. For example, wanting to learn about socialism does not mean I'm a socialist._
|
||||
|
||||
As you can see, this is a pretty long list, and it does not even include the detailed subjects, like 'P vs NP' in theory of computation. But this is absolutely not meant as a 'in the coming year I will learn to'-list, far from it. It is my long-term roadmap for learning. I don't believe I can become a _true_ master at any of them, but for the subjects I can come close to mastering the subject, there can only be a select few. When I decide to learn about a topic, even when I initially finished learning about it, I will probably come back to it later, either to improve my skills even further (for example sports), or to revise my knowledge to get a quick refresher, so I can combine it with other things I've learned. When first picking up a subject, my primary goal is understanding what is needed for near-mastery of the subject. To see my own true competence and not to be over-confident (see: [Dunning-Kruger effect](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect)).
|
||||
|
||||
By the way, notice how there this list is pretty timeless, there is no 'Elm for front-end web development' or 'Haskell for REST API development' on the list. Most subjects would be on this list 10 years ago, and many - if not all - of them will be in 10 years. Learning Elm or Haskell are on my wishlist too, but I see that as separate from learning the fundamentals, the fundamentals I need to actually make the right choices when building with Elm or Haskell. And that's the power of University programs I was too stubborn to see, anyway, back to the topic.
|
||||
|
||||
## Attempting to become a self-taught computer _scientist_
|
||||
|
||||
As of writing this, we are halfway through July 2020, with 5 of my Bytecode team members graduating their Bachelor's degree (and 3 of them continuing to get their Master's too), just starting their summer vacation (for school, many of them spending a lot of time at Bytecode). But for me there will probably not be a lot of vacationing going on. I have decided not to enroll in any University program. I have decided to attempt to become a true computer _scientist_ on my own (well, with the help of books and the internet of course, but I mean without a University). I will try this for around one year to see if it works (and if it does, continue it afterwards). Of course I hope to succeed, but if I don't, I will probably follow some form of formal education that will allow me to fill in some gaps and then allowing me to follow a Master's program.
|
||||
|
||||
## How I will get started
|
||||
|
||||
For my learning journey, I have decided to spend around 60% (12 h/week) of my time on computer science, and 40% (8 h/wk) on a small selection of other subjects. For the computer science part of the journey, the website [Teach Yourself CS](https://teachyourselfcs.com/) has helped me a great deal for how to get started and giving me a roadmap and a list of resources to use. For the computer science part, I will start with:
|
||||
|
||||
- Computer science fundamentals
|
||||
- Learning about how maths and physics allow computers to work the way they do, computer architecture and basic automata;
|
||||
- Dipping my toes into some theories to get to know them (and go in-depth later on);
|
||||
- While learning some discrete maths, physics and electrical engineering along the way
|
||||
- The basics of calculus and linear algebra
|
||||
- Algorithms and data structures
|
||||
|
||||
To do this, I will use the following (in roughly this order):
|
||||
|
||||
- As the resources I will use will be in English, but I only know maths in Dutch, I will use [Brilliant.org](https://brilliant.org/) and maybe some [Khan Academy](https://www.khanacademy.org/) so I can understand the English math jargon and such, this is just to understand maths in English, not to gain new knowledge
|
||||
- When my math skills in English are around the same level as in Dutch, I will use the resources mentioned above to gain some additional knowledge, after that, I will continue by reading Concrete Mathematics by Donald Knuth and see if the lectures mentioned on [teachyourselfcs.com](https://teachyourselfcs.com/#math) are helpful
|
||||
- For learning from the bottom (from the logic gates level) up how computers work, I will use [nand2tetris](https://www.nand2tetris.org/), although I doubt whether this will be in-depth enough.
|
||||
- If I want to continue after nand2tetris, I will use the book Computer Systems: A Programmer's Perspective by Randal Bryant and David O'Hallaron
|
||||
- My first goal will be to learn what's required to understand the maths in The Art of Computer Programming (TAOCP) part 1 by Donald Knuth and then work through this book cover to cover (later I will continue with part 2 to 4A). The reason I chose to go with Concrete Mathematics by Donald Knuth is because he wrote this book to understand the maths in TAOCP
|
||||
- For understanding the basics of the Theory of Computation, I will use the book "Introduction to the Theory of Computation" by Michael Sipser
|
||||
- For algorithms and data structures, I will follow what [teachyourselfcs.com](https://teachyourselfcs.com/#algorithms) recommends, maybe combined with reading the book Introduction to Algorithms
|
||||
|
||||
For the 40% left, I have decided to focus on the following subjects with the corresponding methods:
|
||||
|
||||
- The Latin language (using [Lingua Latina Per Se Illustrata, Pars 1](https://www.bol.com/nl/f/familia-romana/36660250/) and a [guide on how to use it](https://docs.google.com/document/d/1PB5HPz2wBDgqWXPnn5ONgZicOPv8P7LbODvkCxjpI3w/edit) I want to get the basics and then see if I want to continue with Latin or another language)
|
||||
- Golf (taking lessons together with Thomas, one of my closest friends, when we have our handicaps, practice often on ourselves and then see how to progress further)
|
||||
- Meditation (I've got a 1 month Headspace subscription to see whether meditation is something I want to spend time on or not)
|
||||
- Chess (I will follow a short only video course on [Masterclass.com](https://masterclass.com) by Garry Kasparov to learn basic strategies, and then practice online, to get to a rating around 1000, then I'll see how to progress further)
|
||||
|
||||
## Closing notes
|
||||
|
||||
As a final note, I'd like to express that - even though I realize my changed opinion about the value of formal education - I do not regret not following the University path. I do believe it is a lot easier, to have people way with way more knowledge about certain subjects than you ever will, telling you what the best path it to gain some of this knowledge. But I also realize that this approach - whilst easier - is also very rigid, and would probably not allow for me staying at Bytecode or learning about other subjects too. Even if I'd be given the possibility to enroll at TU Delft in September, I think I'd still choose to go down the self-learning path, or at least try it out first for a year.
|
||||
|
||||
_I will update this page and add a log of what I have used for learning, so that others encountering the same issues as I can reference this list and take inspiration from it for their own learning path._
|
||||
|
||||
<br><hr><br>
|
||||
|
||||
## Progress and timeline
|
||||
|
||||
As time progresses, I will add a sort of timeline of my learnings, mostly for myself as reference later, but also so I can advice others on how to progress.
|
||||
|
||||
### Update February 27th, 2025
|
||||
|
||||
I've been getting more serious about low-level programming and game engine development. As with most things I learn, I'm starting at the basics. It's been a blast working with things like Unix sockets, SIMD operations and optimizing for cache hits, and learning more about quaternions in games, custom performant network protocols on top of UDP and how kernels handle syscalls. Learning this theory while at the same time becoming a better game engine programmer feels like a great balance between theory and practical skills.
|
||||
|
||||
### Update September 26th, 2024
|
||||
|
||||
In the last year I have shifted my learning focus on online/multiplayer game development as well as low-level programming. I have familiarized myself with Unreal Engine more, mostly in the multiplayer/online parts of the engine, as well as with custom C/C++ games. Recently, I started to pick up assembly as well as learning more about operating system and kernal programming. As part of my learning, I am catching up on some parts of mathematics as well, as this is crucial in games.
|
||||
|
||||
### Update September 15th, 2023
|
||||
|
||||
The last year and a half, I have mainly focussed on learning philosophy, history, linguistic theory and picking up Modern and Ancient Greek. Since the beginning of this year, I started working on multiplayer games, a lot of things here were new to me, to expand my knowledge I am learning more about Unreal Engine and architectural patterns in multiplayer games.
|
||||
|
||||
### Update January 12th, 2022
|
||||
|
||||
My learning is still mainly focussed on philsophy, history and linguistics. Since I have moved, I have started learning Modern Greek. I have started joining a few online reading groups about philosophy, which have greatly helped me get to know a lot of great people and great thinkers in the past.
|
||||
|
||||
### Update July 21st, 2021
|
||||
|
||||
Nearly all of my learning since the last update has been in the subjects I enjoy learning about the most, namely philosophy, history and linguistics. I have not spent much of my free time on learning about computer science. A small part of my time at Bytecode is spent on computer science learning, some of it theorethical, some of it practical. I also find playing chess and learning more about it to be very enjoyable.
|
||||
|
||||
### Update September 4th, 2020
|
||||
|
||||
My plan was to focus at least 60% on learning computer science. Well, I failed that part. It's been around 10-20%. Although I have learned quite a bit about mathematics in particular, computer science has not been my focus in the last few months.
|
||||
|
||||
Instead, I have spent most of my learning hours on philosophy, with some Latin during the weekends. I really, really enjoy learning about philosophy (Stoicism in particular - crazy how ancient life is so similar to the present in many ways, and how wise some guys were 2000 years ago, anyway, I digress), as it changes your modus operandi, the way you look at the world. It has been beneficial to me on multiple occasions.
|
||||
|
||||
Latin has been way more fun than I imagined. Using [comprehensible input](https://www.youtube.com/watch?v=fnUc_W3xE1w) with Lingua Latina has been awesome. Learning Latin this way even makes working with grammatical cases intuitive, compared to my struggle learning the German case system in high school, having to learn lists of conjugations with limited context.
|
||||
|
||||
I do plan to increase the computer science/mathematics part a bit - maybe to 30-40% of the total - but not to the 60% level I initially planned. One last golf-related thing: yesterday, Thomas and I got our course permission! We will practice over the winter and we are planning to get our handicap 54s in the spring of next year.
|
||||
@@ -1,12 +1,10 @@
|
||||
+++
|
||||
title = "Musik"
|
||||
slug = "experience"
|
||||
slug = "musik"
|
||||
+++
|
||||
|
||||
### Musik
|
||||
|
||||
#### Einleitung
|
||||
Seit meiner Jugend spiele ich Gitarre und Bass in verschiedenen Bands. Ich habe in den letzten 20 Jahren in verschiedenen Musikprojekten mitgewirkt, sowohl live als auch im Studio. Meine musikalischen Interessen reichen von Punk, Rock und Pop über Soul und Jazz bis hin zu HipHop und elektronischer Musik. Ich komponiere, arrangiere und produziere auch eigene Musikstücke.
|
||||
Seit meiner Jugend spiele ich Gitarre und produziere in verschiedenen Bands und Projekten. Ich habe in den letzten 30 Jahren in verschiedenen Musikprojekten mitgewirkt, sowohl live als auch im Studio. Meine musikalischen Interessen reichen von Punk, Rock und Pop über Soul und Jazz bis hin zu HipHop und elektronischer Musik. Ich komponiere, arrangiere und produziere auch eigene Musikstücke.
|
||||
|
||||
#### Projekte und Bands
|
||||
- Pitchdark: meine erste Band. Wir haben mit Covern angefangen und dann eigene Songs geschrieben. Wir haben einige Auftritte in der Region gehabt und ein Demo aufgenommen.
|
||||
@@ -22,4 +20,8 @@ Seit meiner Jugend spiele ich Gitarre und Bass in verschiedenen Bands. Ich habe
|
||||
|
||||
Darüber hinaus habe ich noch in vielen Projekten und mit vielen wunderbaren Menschen zusammen Musik gemacht. Ich habe auch einige Solo-Projekte, in denen ich meine eigenen Songs schreibe und produziere.
|
||||
|
||||
### Fotografie
|
||||
Hier ein Video von Happy Dead Rabbit:
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/gPpK7f3h4wE?si=Jmv4ItZOOZPOPbW3" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
und eins von Racing Morla:
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/vOX5gvCpIS4?si=sxzZTT0HrvBM3SfC" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
@@ -1,49 +0,0 @@
|
||||
+++
|
||||
title = "References"
|
||||
slug = "references"
|
||||
+++
|
||||
|
||||
The references were given in Dutch and translated to English. The phone numbers can be given upon request.
|
||||
|
||||
## Dutch
|
||||
|
||||
Door ir. Jan ten Kate, COO en Product Owner bij Airchip:
|
||||
|
||||
> Na maanden van worsteling en frustratie met ons vorige development team was Luciano Nooijen een ware verlichting. Waar vele ontwikkelaars moeite hebben om zich te verplaatsen in de opdrachtgever neemt Luciano direct verantwoordelijkheid om communicatiestromen, kostenafwegingen, ontwikkelmogelijkheden en werkstructuren helder uit te leggen en uit te voeren.
|
||||
>
|
||||
> Zijn technische kennis en ambitite om bij te leren staan buiten kijf maar echt uniek voor ons was de combinatie van strategisch inzicht en abstracte oplossingsgerichtheid met jeugdige leergierigheid en innovatieve kennis.
|
||||
>
|
||||
> Wij zochten een interim CTO voor onze start-up en Luciano heeft die rol glansrijk vervult. Hij begon met onze tech-stack uit te pluizen (Kubernetes back-end en native Android / IOS app) om vervolgens een uitgebreide plan van aanpak en risicoanalyse te maken. Samen met hem hebben we het ontwikkelteam uitgebreid en een nieuw team gestart die onder zijn leiding ons gehele product tot een hoger niveau heeft getild. Tijdens deze deze periode was de communicatie en tijd-inschatting altijd correct en werd er constant gereflecteerd of ook na zijn tijd bij ons het gedane werk goed overdraagbaar zou zijn, hetgeen zo waardevol is voor elk bedrijf wat met interim krachten werkt.
|
||||
|
||||
Door Pieter van der Oest, co-founder van Dearly:
|
||||
|
||||
> Voor de ontwikkeling van de Dearly app zijn wij een samenwerking aangegaan met Bytecode. Waar zij het technische gedeelte voor ons uit handen nemen en ondersteunen met het maken van technische maar soms ook business decisions.
|
||||
>
|
||||
> In deze samenwerking heeft Luciano de rol als Tech lead en developer.
|
||||
>
|
||||
> Luciano is een zeer gedreven, technische ontwikkelaar. In onze samenwerking zal ik hem omschrijven als iemand die de extra mile gaat en meedenkt over wat niet ‘een oplossing’ is maar wat de ‘best passende oplossing’ is.
|
||||
>
|
||||
> Naast dat hij een uitstekende ontwikkelaar is, is hij ook instaat om het project te overzien en kritisch vragen te stellen.
|
||||
>
|
||||
> Ik ervaar de samenwerking met Luciano als zeer prettig en kan hem zeker aanbevelen.
|
||||
|
||||
## English
|
||||
|
||||
By Jan ten Kate, COO and Product Owner at Airchip:
|
||||
|
||||
> After months of struggle and frustration with our previous development team, Luciano Nooijen was a true relief. Where many developers would struggle to put themselves in the client's shoes, Luciano immediately took responsibility to clearly explain and execute communication flows, cost considerations, development opportunities and work structures.
|
||||
>
|
||||
> His technical knowledge and ambition to learn are indisputable, but what was truly unique for us was the combination of strategic insight and abstract solution orientation with youthful curiosity and innovative knowledge.
|
||||
>
|
||||
> We were looking for an interim CTO for our start-up and Luciano filled that role with distinction. He started by figuring out our tech stack (Kubernetes back-end and native Android / IOS app) to then create a comprehensive plan of action and risk analysis. Together with him, we expanded the development team and started a new team that, under his leadership, took our entire product to the next level. During this period, the communication and time estimation was always accurate and there was a constant reflection on whether even after his time with us, the work done would be easily transferable, which is so valuable for any company working with interim forces.
|
||||
|
||||
By Pieter van der Oest, co-founder at Dearly:
|
||||
|
||||
> For the development of the Dearly app we have partnered with Bytecode. They take care of the technical part and support us in making technical and sometimes business decisions.
|
||||
>
|
||||
> In this partnership Luciano has the role as Tech lead and developer.
|
||||
>
|
||||
> Luciano is a very driven, technical developer. In our collaboration, I will describe him as someone who goes the extra mile and thinks with us about what is not 'a solution' but what is the 'best fitting solution'.
|
||||
> Besides being an excellent developer, he is also able to oversee the project and ask critical questions.
|
||||
>
|
||||
> I experience the cooperation with Luciano as very pleasant and can certainly recommend him.
|
||||
@@ -1,46 +0,0 @@
|
||||
+++
|
||||
title = "Resume Luciano Nooijen"
|
||||
slug = "resume"
|
||||
+++
|
||||
|
||||
My name is Luciano Nooijen, a software engineer with a strong foundation in computer science and a broad range of experience. My work spans from leading technical teams at FAANG-level companies to writing the first lines of code for pre-seed startups.
|
||||
|
||||
I specialize in multiplayer game development, focussing on engine programming and online client development. Alongside this, I am often involved in building supporting tooling, meaning I get to keep my web and backend skills sharp as well.
|
||||
|
||||
Beyond technical implementation, I enjoy contributing at a higher level, ensuring the right business and architectural decisions are made. As a former co-founder of Bytecode Digital Agency, I've worked with many start-ups, helping them make critical decisions and rapidly bring software to life.
|
||||
|
||||
To see my availability and hourly rate, please see the [freelance/recruiters](/freelance) page.
|
||||
|
||||
<br><hr><br>
|
||||
|
||||
{{< mdfile file="assets/blocks/technologies.md" >}}
|
||||
|
||||
<br><hr><br>
|
||||
|
||||
## Game titles I worked on
|
||||
|
||||
{{< mdfile file="assets/blocks/gametitles.md" >}}
|
||||
|
||||
## Professional work experience
|
||||
|
||||
<div class="block-experience">
|
||||
{{< mdfile file="assets/blocks/experience.md" >}}
|
||||
</div>
|
||||
|
||||
<br><hr><br>
|
||||
|
||||
## The human languages I speak
|
||||
|
||||
- **Dutch** (native)
|
||||
- **English** (near-native)
|
||||
- **German** (intermediate)
|
||||
- **French** (beginner)
|
||||
- **Modern Greek** (beginner)
|
||||
- **Ancient Greek** (beginner)
|
||||
- **Latin** (beginner)
|
||||
|
||||
<!--
|
||||
<br><hr><br>
|
||||
|
||||
{{< mdfile file="assets/blocks/workmethod.md" >}}
|
||||
-->
|
||||
@@ -0,0 +1,23 @@
|
||||
<style>
|
||||
.image-gallery {overflow: auto; margin-left: -1%!important;}
|
||||
.image-gallery li {float: left; display: block; margin: 0 0 1% 1%; width: 19%;}
|
||||
.image-gallery li a {text-align: center; text-decoration: none!important; color: #777;}
|
||||
.image-gallery li a span {display: block; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; padding: 3px 0;}
|
||||
.image-gallery li a img {width: 100%; display: block;}
|
||||
</style>
|
||||
|
||||
{{ $dir := string (.Get "gallery_dir") }}
|
||||
<ul class="image-gallery">
|
||||
{{ range (readDir (print "/assets" $dir)) }}
|
||||
{{- $image := resources.Get (printf "%s/%s" $dir .Name) -}}
|
||||
{{- $imageurl := printf "%s/%s" $dir .Name -}}
|
||||
{{- $imagetitle := index (split .Name ".") 0 -}}
|
||||
<li>
|
||||
<a href="{{ ($image.Fit "1600x1600 q50").Permalink }}" title="{{ $imagetitle }}" class="lightbox-image">
|
||||
<img src="{{ ($image.Fill "300x300 q50").Permalink }}" alt="{{ $imagetitle }}" title="{{ $imagetitle }}">
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{ end }}
|
||||
</ul>
|
||||
@@ -4,16 +4,16 @@
|
||||
<h1 class="title">{{ .Title }}</h1>
|
||||
<h2 class="date">{{ .Date.Format "January 2, 2006" }}</h2>
|
||||
<p style="margin-bottom: 0">
|
||||
<b>Reading time</b>:
|
||||
<b>Lesezeit</b>:
|
||||
{{ if eq .ReadingTime 1 }}
|
||||
1 minute
|
||||
1 Minute
|
||||
{{ else }}
|
||||
{{ .ReadingTime }} minutes
|
||||
{{ .ReadingTime }} Minuten
|
||||
{{ end }}
|
||||
</p>
|
||||
<p style="margin-bottom: 0; margin-top: 0;">
|
||||
{{ with .GetTerms "categories" }}
|
||||
<b>Categories:</b>
|
||||
<b>Kategorien:</b>
|
||||
{{ $categories := slice }}
|
||||
{{ range . }}
|
||||
{{ $categories = $categories | append (printf `<a href="%s">%s</a>` .RelPermalink .LinkTitle) }}
|
||||
@@ -41,17 +41,12 @@
|
||||
|
||||
<p style="text-align: center;">
|
||||
<i>
|
||||
Thoughts or questions about this article?
|
||||
Hast Du Fragen oder Anmerkungen zu diesem Artikel?
|
||||
<br />
|
||||
Feel free to drop me a
|
||||
<a href="https://www.linkedin.com/in/lucianonooijen/" target="_blank">message on LinkedIn</a>
|
||||
or a
|
||||
<a href="https://x.com/LucianoNooijen">Twitter DM</a>!
|
||||
|
||||
Dann schreibe mir gerne eine
|
||||
<a href="www.linkedin.com/in/thomas-sindt-555820324" target="_blank">Nachricht auf LinkedIn</a>!
|
||||
<br />
|
||||
<br />
|
||||
|
||||
If you like my articles, please consider subscribing to my <a href="/blog/index.xml" target="_blank">RSS feed</a>.
|
||||
</i>
|
||||
</p>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 44 KiB |