Nathan Brodin
A frontend engineer with a passion for web development, design, and user experience.
About
Originally from the west of France, I've been moving north ever since: interning in Oslo, exchanging in Finland and Sweden, and somehow ending up north of the Arctic Circle in Tromsø, Norway.
I build things for the web, mostly on the frontend, and I care way too much about how buttons feel when you click them.
- Laval — Hometown
- Kokkola — Exchange semester, Finland
- Oslo — 3 internships
- Sundsvall — Exchange semester, Sweden
- Tromsø — Currently here
Social Links
GitHub Contributions
Tech Stack
Experiences
Full-stack engineer at a small Norwegian data analytics company, where I'm effectively the entire engineering function.
I've built two production apps from empty repo to deployed systems (a multi-tenant organization-data platform and a traffic dashboard), owning everything end to end: architecture, design, frontend, backend, infrastructure, CI/CD, and docs. I am also involved in consulting work and other collaborative projects.
I focus on:
- DX: type safety end to end (all-in on TanStack, frontend types generated from the backend OpenAPI schema, CI fails on drift), deliberate library choices, docs and one-command setup, ~1,500 tests with Playwright E2E and checks on every PR
- UI/UX: polished interfaces where every state is designed, refined through tight feedback loops with users
- Observability: OpenTelemetry across 8 repos into self-hosted SigNoz, so traces, errors, and usage back every claim, from the frontend API call down to the DB query
- Ownership: turning one-line briefs into systems ("build a chatbot" became a workspace-scoped analytics agent), and proposing solutions to silent problems nobody flagged: self-hosted runners, observability
I joined a small Norwegian data analytics company on the 4th of August 2025 as a full-stack engineer. The company builds analytical products for Norwegian industries. Everything is self-hosted with Docker behind a global nginx reverse proxy. Every project below involved the same shared plumbing: internal Docker networks, the proxy, and centralized environment configurations.
There is no engineering function around me that reviews code, sets standards, or enforces them. On CapREG and Traffic Dashboard I got tasks and ideas, not a spec, and made every technical and design choice from there.
CapREG
CapREG is a multi-tenant platform for exploring Norwegian organization data, using raw data from Brønnøysundregistrene plus accounting and employee enrichment in-house, stored in ClickHouse. It follows a common Client/Projects setup, with full RBAC. It was the first project I got after the client project calmed down; I started building it in October 2025.
The technical instruction I received were: build the backend with Django, the frontend with React, use Keycloak for authentication. I was given access to the two external ClickHouse sources with raw organization data. For the product itself, I got a drawing of the overview page and a Word document describing rough RBAC (client, project, access group) and the features needed. That's it. No requirements, no design system, no user research, no acceptance criteria.
So I made every technical, architectural, and design choice myself, organized my own work, and implemented it how I wanted. There is no one reviewing the architecture or enforcing standards. The quality of this codebase is a direct reflection of my will and nothing else.
Stack
- Backend: Django + DRF, drf-spectacular for the OpenAPI schema, Postgres for the app, Redis for caching, the external ClickHouse for organization data, Ruff + Pytest.
- Frontend: React 19 + Vite + TypeScript, TanStack Router (file-based, type-safe routes), TanStack Form/Table, Tailwind 4 + shadcn/ui, Paraglide for EN/NO translations, T3 Env for typed environment variables, Orval to generate the entire API client and React Query hooks from the backend schema.
- Contract: the OpenAPI schema is committed and generated from the code; the frontend's types are generated from the schema. CI fails if either is out of date. Backend and frontend can't silently drift.
- Auth: Keycloak JWT validation on the backend, redirect to the company's Keycloak on the frontend, separate clients per environment.
- Deployment: Docker, staging and production environments on the same server, containers on a shared Docker network behind the central reverse proxy.
Why: type safety end to end, one schema instead of hand-written API types and hand-pasted tokens, and boring, well-supported tools. I wanted CI to catch the failure modes instead of users. I also made the most of libraries rather than custom implementations.
What I built
About 1,090 commits in total, 929 non-merge, nearly all mine; 229 release tags so far.
- Oct 2025 — the repo: environment management that fails the build when not configured, pnpm enforcement, TanStack Router setup, Docker/nginx, Keycloak login, i18n, theme and shadcn, header, overview skeleton.
- Nov 2025 — frontend and backend talking; ClickHouse connection; the organizations endpoint with filtering, sorting, search, pagination; the data table, sidebar filters, CSV export. The core query design landed here: one SQL statement does the workspace scope + filters + full-text search + parent/contact inheritance + sorting + pagination + aggregate metadata (total, employees, ...).
- Dec 2025 – Jan 2026 — organization profile (header, information, structure tree of parent/subunits, activity, charts and metrics), RBAC on the manage pages, workspace breadcrumbs, seed-data commands (DX), Redis cache, a 10x improvement of the organizations query, session persistence, frontend caching.
- Feb – Jun 2026 — notifications inbox and preferences, saved views, the analysis pages (financial, municipalities, value creation, registrations and bankruptcies, per-organization charts), a second-generation organizations query (a pre-computed ClickHouse table, frontend prefetching, query timing and logging, auth performance work), certificates, help page, and repeated rounds of content/translation changes from my manager's feedback.
- Jul – Sep 2026 — the AI chat (below) and historical snapshots.
The AI chat
My task was literally "build a chatbot". What I built is an analytics agent scoped to the user's current workspace:
- An async streaming agent loop (tool-calling, SSE in the TanStack AI chunk format) against a self-hosted OpenAI-compatible model.
- The system prompt is built per request from the user's actual workspace: tenant, register, access group, municipality codes, NACE business codes, snapshot date, and the years of history that exist. It teaches the model what the data means and what it doesn't.
- The toolset covers read-only SQL over the workspace-scoped relations and a multi-year variant, a column-semantics lookup, workspace headline figures, single-organization detail, organization search, value-creation series, and reference-code translation.
- Safety: the agent's SQL is validated before it runs, single statement only, SELECT/WITH only, no DDL/DML/connection keywords, a table allow-list limited to the scoped relations plus the model's own CTEs, reserved CTEs can't be shadowed, comments and string literals are stripped before scanning, LIMITs are clamped, results are truncated, and raw ClickHouse errors are rewritten so the model doesn't receive the full expanded query and hosts. The query is wrapped in the same scoped CTEs the UI uses, so it physically cannot read outside the user's workspace.
- Per-tenant feature flag, conversation persistence, reasoning display, tool-call UI, and Vega-Lite charts that the frontend themes.
It's the most technically interesting thing I've built here and it's working extremely well. I refined the context, prompt, and tools over and over, to make sure the outputs were as good as possible.
The practices I chose to make non-optional
- ~1,530 backend test functions across ~67 files at the time of writing.
- Ruff lint/format, ESLint/Prettier, Django system checks and migration checks, frontend build check.
- CI on every PR: backend lint + tests (with CTRF reports published to the PR) + Django checks, frontend lint, OpenAPI schema sync, generated-types sync, frontend build.
- CD: every push to main auto-deploys to staging with an automatically bumped semver tag; production deploys only on a published GitHub release. Every deploy backs up the database first, then does
docker compose up -d --waitso health checks gate it. - DX: a Makefile with tasks for setup, code generation, pre-commit checks, seeding and admin promotion; READMEs; and agent instructions in the repo. Generated API client. i18n discipline (no hardcoded strings). A frontend Playwright suite with ~15 specs for auth, RBAC and workspace flows.
Technical craft
Design and UX
- Feature-first layout: each route owns its page and a colocated components folder (overview table, filters, org profile sections, chat); shared UI primitives built on Base UI and reusable form fields live separately.
- One form system: TanStack Form is composed once into typed field components (input, select, combobox, switch, file upload, textarea, business codes, municipalities, registers, users) plus submit/cancel form components. Pages don't hand-roll forms; server errors get mapped back to fields centrally instead of a generic banner.
- Two table layers: a generic table for management pages and an infinite-scrolling table for the organizations list. Table state (pagination, sorting, search) lives in URL search params; column visibility and order persist per table to localStorage with try/catch around parsing.
- Filters are URL state, not component state: a filters hook reads and writes typed search params inside
useTransition, resets pagination on change, and keeps the scroll position so the list doesn't jump. Multi-value filters are comma-separated strings with shared parse/format helpers; saved views and filter counts sit on top of that. - The workspace is first-class in the URL (
/{tenantSlug}/{accessGroupSlug}/{registerSlug}/**). Resolution is URL → zod-validated localStorage → first API result, with stale-storage detection when the tenant in the URL differs from the stored one, and permission-gated selectors. - Loading/error/empty states are app-level rather than reinvented per page. Chat adds its own empty state, streaming/tool-call states and a context-usage widget.
- Dark/light/system theme through a small custom provider; components use semantic tokens rather than raw colors.
- Accessibility is decent but not systematic: over 200
aria-*/role=attributes, real buttons and labels, keyboard behavior from Base UI, copy buttons vianavigator.clipboard. - Fully responsive design: a
matchMedia-based mobile hook drives the sidebar (a slide-over panel on mobile), hides hover tooltips on touch, and several tables/charts switch layout or axis density on small screens. A dev-only breakpoint indicator shows the active Tailwind breakpoint while building.
Web standards and browser-native choices
- The platform is the state store: URL search params, sessionStorage for the sign-in return path, localStorage (with zod parsing) for workspace preference, column prefs and chat reasoning effort. No Redux/Zustand for server or filter state.
- Native WebSocket for live notifications; each message triggers targeted query invalidation for suggestion and workspace data. Honest weaknesses: no reconnect/backoff/heartbeat, and the access token travels in the query string: if the socket drops, it stays down until the component remounts.
matchMediafor the mobile hook;ResizeObserver+MutationObserverto redraw a canvas decoration;navigator.clipboard;usehooks-tsfor debounce and resize where it earns its place.- React 19 + React Compiler enabled globally, with TanStack Router preloading routes on intent.
useTransitionis used for filter/table navigation and dialog submissions, route guards fetch data before rendering instead ofuseEffectfetching, and compiler opt-outs are explicit where third-party patterns can't be analyzed. - Chat streaming uses a fetch-based SSE transport (so POST + bearer auth work) instead of the browser
EventSource, plus a custom localStorage persistence adapter and history hook. The Django side speaks the library's streaming chunk protocol over a streaming response, and tool execution runs in a worker thread because the ClickHouse client is synchronous.
Caching and invalidation
- Client (TanStack Query). Defaults in the global query client, no query retry (mutations retry once). Per-endpoint overrides are declared centrally in the Orval configuration rather than scattered in components.
- Client invalidation. Mutations invalidate by generated query key. Workspace switches invalidate through a predicate that clears everything under the tenant's API prefix. Notification preferences are written directly into the cache with
setQueryDatafrom the mutation response instead of refetching. There are no optimistic updates anywhere: I chose refetch-on-success over rollback complexity. - Server (Redis, with a real invalidation story). The organization pages are cached through a custom decorator instead of Django's built-in page cache. Two things make it safe: (1) the workspace access check re-runs before the cache is consulted; (2) because cache keys are URL hashes that can't be enumerated on write, each tenant has a monotonically increasing "generation" counter folded into the key prefix. A write bumps the generation, which orphans every cached organization page for that tenant at once; old entries die by TTL.
- Auth caches. Keycloak's public key is cached, decoded JWT payloads are cached per token until expiry, and the User object is cached per request path.
- Precomputed ClickHouse table. A scheduled job rebuilds the current snapshot from the raw register + other sources tables. The API reads the precomputed table, so the heavy enrichment happens once a day instead of per request; this led to very good performance (~0.3s queries) even when reading 20 million rows with heavy filtering.
- HTTP/browser. The frontend nginx caches hashed assets, gzips text, sets security headers and a CSP that whitelists Keycloak, and proxies WebSockets.
Library usage
- Orval is leaned on hard: tags-split client, zod schemas, MSW mocks, per-operation query options, and a custom request client for the generated API layer.
- TanStack Form's composition API is used as intended; because it's wired once, every form gets the same fields and error rendering.
- Base UI was a deliberate choice over Radix (migrated in Jan 2026): accessibility behavior from the library, styling and composition ours.
react-oidc-contexthandles Keycloak redirect/silent SSO; the refresh queue is ours on top.
What's uncertain
A few test users only to this day. I can see the risk that I optimized for the codebase I wanted rather than the product the company needed. I also can't prove the codebase is as good as I think it is: nobody reviewed it.
What I do know: I can take a domain I don't know, an empty repo, and a one-page brief, and end up with a deployed, tested, documented, type-safe system that I'm proud of: mostly alone. The project is stable, working, fast, and accurate.
Traffic Dashboard
The Traffic Dashboard is a dashboard for Norwegian traffic: flights data and cruise ships data combined into one view. It reads from four databases: a local Postgres (auth/users), an internal Postgres (flights + taxonomies), an internal MariaDB (cruise voyages and ships), and an internal ClickHouse (occupancy data).
Before me, a student had built a very rough dashboard with flights only in 2 weeks. My task was one sentence: "Build the traffic dashboard for flights and cruises" with rough product specs. No stack discussion, no design. I chose the stack, the architecture, the design and everything else myself, and then refined it based on feedback. To this day, 1 external user has used the application.
Stack
A full TypeScript Turborepo monorepo: pnpm workspaces + Turborepo; a web app (React 19, TanStack Router, Tailwind 4 + shadcn); a server app (Hono + oRPC, Drizzle ORM, Better-Auth); and shared packages.
I built it this way to remove the headaches I had with CapREG:
- Backend/frontend contract — oRPC gives end-to-end type safety between server and client, with no OpenAPI → codegen round trip to keep in sync.
- Auth — Better-Auth with the Keycloak OIDC plugin, cookie sessions, and a Keycloak group check. No hand-rolled JWT storage.
- Python — I didn't want another Django codebase. Python deeply lacks the safety and tooling I want for maintaining a clean codebase; one language end to end means one type checker.
What it does
Filters by destinations and origins, time range and months, transport mode, passenger counts, and stay duration. It shows KPIs, table with details, traffic trends, traffic distribution, top origins, and a routes map with tooltips. Cruise-specific work: filtering out cancelled and non-cruise vessels, stay duration, estimating a ship's capacity from its own historical maximum passengers, and occupancy rate.
The data plumbing that took real work
- Cross-database joins are impossible, so flights and cruises are fetched separately and merged in the API layer.
- Hand-maintained Drizzle schemas for the read-only external databases; the app is never allowed to write to them (enforced by the ORM config by construction).
- Keyset pagination per database: Postgres row-value comparison; MariaDB has no tuple comparison, so the keyset is expanded into OR/AND.
- Occupancy is pre-computed into a lookup table refreshed from ClickHouse + Postgres, so the dashboard doesn't hit the raw data on every request.
Technical craft
Design and UX
- One shared validation schema defines every filter and is reused by both the client-side route validation and every server procedure. That single decision keeps client and server in agreement and makes every filter state a shareable URL.
- All filter writes go through a small navigation helper that does functional search-param updates without resetting scroll. Server state is TanStack Query through oRPC query utils; everything ephemeral is local
useState. There is no global store. - Loading/error/empty: card skeletons, 50 skeleton table rows, 15 tree rows, shimmer overlays, a shared empty state per card, a global query-error toast with a retry action, and a WebGL-failure fallback on the map. Previous data is kept visible while new data loads on the 16 data queries.
- Responsive choices are explicit rather than fluid: fixed card heights on mobile, a viewport-locked grid at xl with escape hatches for short screens, sticky table columns for the numbers that matter, and minimum tap targets on coarse pointers.
Web standards and browser-native choices
- URL/History as the state machine,
IntersectionObserverfor infinite scroll,ResizeObserverfor table filler height and tree sizing,MutationObserver+matchMedia+getComputedStyleto keep MapLibre's canvas style in sync with the CSS theme.
Caching and freshness
- Server process cache. A small in-memory LRU cache holds three computed things: the location tree keyed by date range + mode, a ship-capacity map, and data-coverage bounds.
- Precomputed table. A precomputed occupancy lookup table is built from two datasets plus the company mapping, averaged per airport/month/flight-type over the last 3 years with national fallbacks, then replaced in chunked inserts.
- Query-layer caching in SQL. Keyset pagination instead of COUNT, aggregates batched with the rows in
Promise.all,MAX()/AVG()pushed into SQL, module-level lookup maps built once at import, and client memoization keyed on the full filter tuple.
Library usage
@orpc/tanstack-queryis used as designed (queryOptions,infiniteOptions, client + query client in router context).- TanStack Table v9 API: the feature-based table setup and selector-based subscriptions so only the sorting/expanded state that changed re-renders.
I built the first meaningful version over about two and a half months (~168 commits, all mine, Jun–Sep 2026) and I've been refining it from feedback since.
Client project
This was consulting work for an external startup building a data analytics modeling tool: clients upload datasets, build models, and generate reports. It was already largely developed when I joined, having been built over several months prior.
Work came as heavily prescriptive tickets from the client. I never saw a product strategy or a technical plan for the system. The project lacked established software engineering practices, and work was largely restricted to executing narrow fixes.
I started in August 2025. My first weeks were literally the tickets I was given: rename "this" to "that", fix casing, fix a page name, update the logo. That set the pattern of the whole project: tickets priced in hours, with limited space to question whether the architectural foundation underneath was sound.
I made around 223 out of ~1,325 commits in the repo.
- Main frontend ownership — the biggest page in the product had grown into a single 1,635-line component. I split it into a container plus focused section components (151–535 lines each) so it was actually maintainable and could build upon it.
- Feature work across the UI — built new pages from scratch, wired forms and tables to a changing backend API, fixed state and pagination bugs, and added exports.
- Auth and frontend infrastructure — token refresh, user avatar, scoping by client, local development access and email handling.
- Ops — raised the nginx and Django upload body limits, fixed file serving in prod, fixed the logging package, cache invalidation.
- Quality — lint/format cleanup and a code-quality GitHub workflow (Aug 2025). In Sep 2025 I added the first E2E workflow in CI with a Playwright journey suite covering real user journeys. I also added CTRF test reporting, and then spent a long tail keeping the suite passing as the API and pages kept changing.
- Backend/API when needed — schema generation, settings, filtering and pagination on list endpoints, adapting forms to new API specs, and payload handling.
- My last push (Sep 2026) — server-side pagination. I added search/ordering/pagination across the main list endpoints and the pages that use them, and removed the frontend's oversized fetch limits (24 occurrences across 16 files) in favor of paginated queries. The frontend was making insane calls, and the backend gathering insane joins, which would time out a six-minute fetch request just to display 10 rows of 5 columns.
I formally audited the codebase and found significant architectural and security gaps, which I documented and escalated to the team. Beyond that, it was a complex legacy codebase with a ticket process that heavily rewarded one-to-five-line PRs over system architecture. The fixes that were authorized were often symptoms; the foundational code remained chaotic.
Self Hosted Runners
The company was hitting the GitHub Actions usage limits, and a large part of that was me: my CIs run lint, tests, schema/type checks and builds on every push and PR (CapREG, the client project, later Traffic Dashboard). Paying GitHub for minutes we could get from our own server didn't make sense, and the limits were starting to block work. So I set up self-hosted runners on a company server.
I deployed GitHub Actions self-hosted runners with Docker Compose: 12 containers of myoung34/github-runner, registered at the organization level so every repo in the org can use them. Then I switched my pipelines from runs-on: ubuntu-latest to runs-on: self-hosted and shared the setup with the team.
- A GitHub Actions workflow deploys over SSH with a runner-count input (default 12). It reads configuration from the server, scales the compose service to that many containers, and prints the result.
- Runners are ephemeral, auto-update is disabled, names get random suffixes, and container logs are capped.
- A cleanup script plus a daily scheduled workflow deletes offline runner registrations. This one was learned the hard way: when a container dies hard, GitHub keeps the registration forever. Without cleanup the list grows until it hits GitHub's 10,000-runner limit and then no new runner can register. The script skips offline-but-busy runners so in-flight jobs are never cancelled.
Observability
In September 2026 I pitched the idea of implementing Observability at Capia as I saw that we had two painpoints that could be resolved with OTel:
- The client project was falling apart, with slow queries and errors coming from all places, and it would only be reported to us with screenshot of the error toast saying "Server error". We had to look through the raw Docker logs to try to understand what was going on.
- Management started giving away accesses to the different services we've been building, but we didn't knew if they actually used them, what they were using, if everything was working...
So I proposed the idea of OTel, and received great feedback that it could be very useful.
I spent a week setting up the OpenTelemetry SDK across the 8 repos of the company which all have a different tech stack, so trying to have the same implementation, and sending the same logs and traces to our collector. I've setup a self hosted Signoz on the server to collect and view all data, and setup the MCP server so I can have my LLMs build dashboard and inspect logs.
Now we have traces going from the frontend API calls all the way to the individual DB calls, we have error alerts, analytics for users... We finally know what's going on at Capia.
- TypeScript
- React
- TanStack
- Tailwind CSS
- Django
- Docker
- Nginx
- UI/UX Design
- Design System
Worked across multiple AI products used by 100k+ users. It was my end of studies internship, of 6 months, that counted toward my Master's degree (graded 92/100).
- Managed to push to production 15+ massive features across 3 frontend applications
- Migrated a production app from Gatsby to Vite, cutting build times by 60%
- Built a full E2E test suite with Playwright. With 350+ tests across browsers, and a CI that runs under 90s with caching and sharding
- Wrote technical docs and analysis reports, generally tried to make the DX better for the team
From February to July 2025, I completed my end of studies internship, once again, in the same team at DNB. My previous project was in production, with ~100 users active users and ~2000 potential users, just for an internal tool.
This time, I had to work across the two previous projects I created, and an existing project of the team.
This one was the "Chat Framework" (CUF), which is the base of all chatbots that I created the admin panel for. It does ~50k daily conversations. The frontend was quite disappointing, with a lot of mess, legacy libraries, no formatting/linting, lots of complexity, no consistency... it was built by external backend consultants, in a team with no engineering culture or software engineering practices.
The main issue to work with it was that it communicates with an external backend "Boost AI", which is what powers the chat solution. This was a blackbox of functionalities that we could not recreate. Like for example the chat can show a credit card select, but we don't have anything to make it display in the chat during development, and the testing team only could do it once deployed.
What I did:
Code Migration
CUF was from 2018, still on React 15, deps not updated since, install failing, no valid docs, 30s+ startup with useless logs, TS/lint errors everywhere, one-line change reformatting whole files, inconsistent DEV/UAT/PROD configs and silent feature toggles. Components were the opposite of best practices: 500+ line files with 10+ useEffects, dead code, unused branches, console.log spam.
An offshore consultant was migrating Gatsby to Vite and everything broke, with no tests or mocks to understand expected behavior (I discovered the chat could show image responses 3 months in because someone reported it broken). My first task was fixing bugs she couldn't fix, mostly by diffing HTML output old vs new to find the tiny detail breaking a feature.
It also included Eufemia 9.x to 10.x upgrade: removed legacy // Workaround to fix Eufemia issue hacks, manual QA loops (testers report, we fix, repeat – works but leaky for subtle regressions). One Tooltip bug wasn't ours at all, it was in Eufemia itself. I reported it, saw their backlog being full, so I fixed it myself and made a PR, which was published in days, the team really like that.
The migration shipped with zero functional change, so ironically my first contribution to reach production was invisible to users.
CAP History
The new features added on top of my work I initially mentionned was having an history of the changes applied to the Chat Admin Panel. Like if someone updated a link, it should be preserved in the history. The data used to come from AWS Parameter Store (full config + history in one response), then team moved to AWS AppConfig which has no history, so backend had to fetch each old version separately.
The proper fix belongs in backend, but I was available and reknowned to be fast so raw unprocessed data got pushed to frontend for me to figure out. I built clean scoped history (each CAP page shows only its fields' history), demoed, then got told to show all history on every page because backend assumed scoped restore was impossible with AppConfig structure, so I ripped out my elegant filtering for a generic downgrade. Then endless back-and-forth with backend + QA with no clear spec, implement → partial feedback → revise. Still ended well: pagination, better UI/UX, better than original, shipped to prod to ~10 internal users, stakeholders happy.
One month in I already had 2 features in prod, vs 0 in my previous 7 months.
Why people don't care about users?
In Smartdocs, a small link-parsing function I wrote last year to make links clickable in an assistnant message broke when backend started sending Markdown (links already formatted). It was reported Dec 2 2024, I diagnosed + fixed + tested + committed in 3 minutes in March. Even a stranger to the codebase could have done it in minutes. So why did a user-visible bug sit for 3 months, while it was an actual issue reported by many?
Dark mode
My first customer-facing feature (Aino lives in the mobile bank app, 1.5M users): business wanted dark mode in chat. So I pitched two options: quick per-component manual recolor (fast, messy, tech debt) vs proper dark mode inside Eufemia then delete our custom styling (clean, consistent, but wait on Eufemia team + lose control). After lots of same-content meetings with different people, business chose quick now, proper later (Eufemia planned to support dark mode before summer). I had it working within a week in March, but lauch was stuck for 2 months: backend theme API changes given to a new hire (normal he was slow, wrong task to rush on), and there was a backend perf crisis at that time so deploy freeze. Then all of the sudden they started intensive testing on Monday with planned deployment on Thursday in June, so I received ~10 edge bugs in 3 days that I had to fix fast (my fast reputation helped) and the issue with that I could not reproduce anything. Shipped June 5th.
Technical side, why it was tough: theme JSON naming makes not much sense (secondary_background with no primary_background, secondary_text_in_chat, etc.), there was lots of per-component overrides with !important for hover/active/disabled but forgetting focus or hover+active combos (light colors leaking in dark mode), mixing custom vars with hardcoded var(--color-white), it was only for Aino so I had to make sure I was not breaking the 4 other bots, and ~30 hidden components only QA can trigger (that credit-card select again). Blindfold painting: QA finds broken component, I fix without seeing it, hope deploy looks right.
In Smartdocs a few weeks later, I ripped all custom colors from ~25 components, replaced it with 4 lines of code at app root to override Eufemia at the start. And then tried dark mode in a few hours, no edge cases. So I saw the issues with CUF, and improved Smartdocs as I always want to make the best codebase possible
Smartdocs DX
I always tried to make Smartdocs the best codebase possible: env setup was confusing backend devs despite README, so I reworked commands to pick local vs deployed backend with clear console logs (10-min task, zero questions after). Bumped all deps with breaking-change fixes (the thing CUF never did in 7 years). Added 75 Playwright E2E covering full user flows (send message, settings, language switch...) plus regression tests for old tricky bugs (send → don't wait → open history → send in old convo failing on loading states) plus visual screenshot diffing. Optimized from 12 min (75×3 browsers Chrome/Firefox/WebKit) down to 5 min (from 4 min before) via parallel + caching + structure. After that QA never reported a single issue, and I could refactor fearlessly. Left big 90-file cleanup PRs, best docs and DX I could.
I also wrote new CUF docs: how it works, what to rework, links to lib docs and best-practice articles.
Since this internship counted for my master degrees, as it replaced an entire semester, it got graded 92/100 for my work, presentation and report. Which for French schools is an extremly good grade.
- TypeScript
- React
- Redux
- CSS
- Playwright
Built the frontend of a GenAI chatbot platform used by 100+ users. From blank repo to well-tested app (92% coverage, Storybook, documented architecture). Focused on developer experience: clean code, consistent styling, and full test setup.
From April to August 2024, I returned to the same team at DNB. I got to see my previous project live in production, with ~10 users, and developers that had built on top of it without issues!
Once again, I started right when they started a new project, so I got to build a brand new frontend mostly alone. This was a new chatbot application, but it's not sharing the same base as the other chatbots that I got to build the admin panel of, this one was an LLM RAG chatbot, built from the ground up. It's called the Smartdocs Platform: a multi-tenant chatbot that plugs into DNB's internal SharePoint sites so teams can ask questions over their own documents. First tenant was "AML Justina" for the Anti-Money Laundering team, who otherwise had to dig through hundreds of pages of docs.
Requirements for the frontend codebase got elevated a bit by my mentor, with TypeScript (Vite), and Redux with RTK Query for data fetching, Storybook to render components in isolation with mocks, and Playwright for E2E testing.
Once again it was collaboration with the same UI/UX designer for the Figma sketches and the feedback, with the backend and AI engineers to make it work, and all the meetings that come around. We started 6-7 devs on the frontend splitting components, and ended up just 2 of us.
What I actually built there (257 out of 362 commits, top contributor):
- Components: started with the
ChatForm(textarea + Send button, which sounds trivial but meant autofocus on load, Enter to send vs Shift+Enter for newline, blocking empty sends, blocking send while streaming, clearing after send, theme variables), then 19 other components the same way. Same tricky Eufemia overwrites as before, using deep CSS selectors found via Chrome DevTools to force the Figma look. - E2E testing, with 107 tests in total, 92% coverage.
- API integration, 3 rewrites: I was first asked to work without a backend and use Microsoft's AI chat template instead, as they would follow it. I made it clean and working, with nicely typed logic. Then they shared the OpenAPI specs, which largely differed from the template, so I adapted everything (and found Prism to generate a mock server straight from OpenAPI, so I had a backend working sending mocks). Then they shared the first draft of the backend, which did not match the OpenAPI specs at all, so I refactored a third time and took the initiative to rewrite the OpenAPI docs to reflect reality. The solution architect also asked me for a Sequence Diagram of the app flow to make the UI logic understandable.
- Streaming: I implemented SSE streaming on top of a backend that couldn't stream at first, so the frontend had to handle both. That meant stripping
data:prefixes, accumulating chunks, splitting on newlines, buffering incomplete JSON untilJSON.parsesucceeded, and appending content progressively. Gated behind feature flags with a type-safeuseFlaghook I wrote (only boolean keys from the config type show up in autocomplete, typesafety all the way of course). - Theme customizer: the designer asked how to do per-tenant theming (primary color, secondary, etc.) with no spec as it was difficult for her to do it over Figma, so I built a
/themepage where she could try colors live, different icons and copy the resulting JSON. The custom bot icon was the tricky part since it came from the backend, which I solved withapi.util.updateQueryDatato patch the cached config from the frontend. - Smaller stuff: chat history took 2-3 hours to implement as the backend guy was very quick to make it available in the backend. We add a settings menu with a single setting for a zoom slider for font size that I thought shouldn't exist (browser Ctrl+ already does it, but I built it anyway).
- PDF export, the hardest task: first attempt rasterized HTML to PDF, style preserved and designer happy, but links were dead (citations to source docs, the key feature) and page breaks cutting messages in half. Started over with react-pdf (custom
<Document>/<Page>/<View>/<Text>tags, no HTML reuse) to fully control layout, links and breaks. There is very little docs online, lots of blind experimenting, but proud it worked perfectly. - Docs and transfer: comprehensive README (install, run, how to add component/localization/tests...), then a Knowledge Transfer session.
- Non-technical: mentored 2 high-schoolers with another intern for 3 days with ~20 progressive Python exercises I prepared, plus final presentation coaching.
But once again, the project did not reach production once I finished my internship, so I couldn't see users using my work.
- TypeScript
- React
- Redux
- CSS
- Storybook
- Playwright
Shipped an internal admin panel from scratch used by 10+ users, based on Figma designs. Small project with basic tech (CRA + JS), but good foundations. I handled everything from UI, API integration, auth, and deployment. My first production app.
I joined DNB for the first time as part of the summer internship cohort of 2023, with the "Emerging Technologies" team, and did 2 other internships in the same team later on.
From end of June to end of August 2023, I got assigned to work on the frontend of the "Chat Admin Panel". It was the summer between my 2nd and 3rd year of Engineering studies. For context, DNB has multiple internal and external chatbots, which are domain specific chatbots to answer user questions. These are pre-LLM chatbots. Each solution is owned by different teams and product owners across the bank. They all shared the same base, but had different themes, icons, help resources, pre-defined questions, which were stored internally in some AWS configs.
When these product owners wanted to change something for their chat solution, they had to contact the team, wait for a developer to mess with AWS to find what to change, and wait for the feedback. So I developed, from scratch, the frontend of the "Chat Admin Panel", which is a web app to empower the product owners to do the changes to their chatbots by themselves, in an intuitive way (we're not giving them raw access to AWS of course lol).
So I worked with the UX/UI designer of the team, that had all visuals of the application on Figma, and the backend engineer, that created the API to handle the data, and I was by myself on the frontend.
I started the project from scratch, with a bare CRA app in JavaScript, Emotion for styling, Redux for state, Axios for API calls. It was my first "real" React app, as I had only worked on a Chrome extension made with React before that, a simple "Password Manager". So this was quite a simple CRUD app, with basic React Router navigation. It was not meeting the high production standards and modern stack that I now master, but this was still a very good application which received very good feedback.
One challenge was with styling: DNB has a component library ("Eufemia") that was the base of the app, but the Figma sketches modified it a lot, with different colors, spacings, and even light/dark mode... while the component library has not been built to be modified (like with shadcn/ui for example). So it was quite tricky to overwrite the CSS and make it consistent. I also handled Microsoft auth and manual AWS deploy (S3 + CloudFront + Route 53 instead of Amplify).
This is the only project I worked on during that internship, me working alone on the frontend without code reviews or feedback, with a backend that was given, but lots of good back and forth with the designer. The project was not pushed to production yet.
- JavaScript
- React
- Redux
- CSS
Education
Education
My Master's in Software Engineering, from ESIEA, the 7th best graduate engineering school in France. Graduated with 92/100.
The first few years relied heavily on mathematics, algorithms, physics and electronics, moving toward programming and broader software development and software engineering practices at the end. Plus 3 internships (a full year of real work experience by graduation) and 2 exchange semesters.
Five years, ten semesters, 300 ECTS.
Each semester is split into blocks of courses, and each course has a coefficient: its weight in the block average. To pass a semester, you need at least 10/20 in every block, and no course under 6/20. Pass both semesters and you pass the year. But a single course under 6/20, or a single block under 10/20, and you re-take the entire year. Not just the course on the side of your next year, not just the semester: the entire year.
Grades are out of 20, with the class average for context. The letters are the ECTS scale, which ranks you within the promotion: A is the top 10%, B the next 25%, C the next 30%, D the next 25% and E the last 10%.
Year 1 (2020–2021)
Mostly mathematics, physics and electronics, with an introduction to programming. I got humbled early: 8.50 in functions of a real variable, 8.80 in analogue electronics, and my only E of the whole degree in the first math/computing block. The personal development courses carried my average, turns out talking about yourself is easier than integrals.
Semester 1 — average 12.54/20, 30/30 ECTS
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF1031 | Basics of IT and programming | 3 | 11,36 | 11,18 | |
| ES-LAB1413 | Software practicals | 2 | 10,91 | 10,4 | |
| ES-MAT1051 | General Algebra and Arithmetic | 3 | 10,98 | 12,00 | |
| ES-MAT1052 | Logic Sequences, Functions of a real variable and approximation | 3 | 11,11 | 8,5 | |
| ES-PLU1192 | IT catch-up workshop | 1 | 11,25 | 8,75 | |
| Final Result | Mathematics and Computing (S1) | 12 | 11,12 | 10,38 | E |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-HUM1091 | Personal Development Project | 2 | 16,20 | 20,00 | |
| ES-LAN1081 | English Semester 1 | 3 | 12,86 | 15,07 | |
| ES-TEX1065 | Personal Development Workshop | 1,5 | 16,56 | 17,00 | |
| ES-LAN1183 | Chinese | b | — | — | |
| ES-TEX1066 | Spoken and Written French | 1,5 | 15,84 | 15,6 | |
| Final Result | Personal Development (S1) | 8 | 15,07 | 17,14 | A |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-ELE1071 | Analogue Electronics | 3 | 12,28 | 8,8 | |
| ES-LAB1411 | Measurements, electronic components and robotics | 3 | 13,35 | 12,00 | |
| ES-PHY1021 | Point Mechanics | 4 | 11,66 | 13,00 | |
| Final Result | Physics, Electronics and Systems (S1) | 10 | 12,35 | 11,44 | D |
Semester 2 — average 13.73/20, 30/30 ECTS
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF1032 | Algorithm design | 3 | 11,46 | 15,37 | |
| ES-LAB1414 | Software project | 3 | 11,34 | 13,62 | |
| ES-MAT1053 | Linear algebra | 2,5 | 11,42 | 13,5 | |
| ES-MAT1056 | Local study of functions and Integration | 2,5 | 10,73 | 11,5 | |
| ES-PLU1195 | Digital reinforcement | 1 | 10,50 | 11,97 | |
| Final Result | Mathematics and Computing (S2) | 12 | 11,19 | 13,45 | C |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-HUM1092 | Personal Development Project | 2 | 14,73 | 15,75 | |
| ES-LAN1082 | English Semester 2 | 2 | 12,77 | 16,00 | |
| — | Chinese (optional, bonus) | — | +0,23 | +0,56 | |
| ES-TEX1067 | Personal Development Workshop | 2 | 13,97 | 14,5 | |
| ES-TEX1068 | French expression | 2 | 14,58 | 16,00 | |
| Final Result | Personal Development (S2) | 8 | 14,24 | 16,12 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-ELE1072 | Analogue Electronics | 3 | 11,85 | 12,3 | |
| ES-LAB1412 | Analogue Electronics Project | 2 | 13,20 | 10,67 | |
| ES-PHY1022 | Electromagnetism | 3 | 11,82 | 12,00 | |
| ES-PLU1198 | Interdisciplinary project | 2 | 12,60 | 13,67 | |
| Final Result | Physics, Electronics and Systems (S2) | 10 | 12,26 | 12,16 | D |
This was during Covid era, where a bunch of "new adults" like me had to actually start doing their homeworks and work, compared to the easy life of high school. My school had a record of students that were on their way to not pass the semester, so our responsible teacher sent an email to the ~20 students to tell us to wake up or it will be over for us. I did woke up.
Final average for the year: 13.13/20.
Year 2 (2021–2022)
Still a lot of math and physics, but computer science started taking real space. Also my first internship: a short "discovering the world of work" one, graded 19/20, and the teacher saying this was the best presentation she's ever seen (my Powerpoint was truly beautiful).
Semester 1 — average 13.52/20, 30/30 ECTS
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF2031 | Advanced algorithmics | 6 | 11,75 | 14,25 | |
| ES-MAT2051 | Algebra and Geometry | 3,5 | 14,50 | 16,00 | |
| ES-MAT2052 | Curves and surfaces | 3,5 | 10,24 | 10,5 | |
| Final Result | Mathematics and Computer Science | 13 | 12,08 | 13,71 | C |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-HUM2091 | Personal and professional skills project | 2 | 16,17 | 17,00 | |
| ES-LAN2081 | English | 2 | 14,15 | 15,34 | |
| ES-PLU2192 | Scientific and Technical Project | 2,5 | 13,64 | 14,00 | |
| ES-TEX2065 | Communication techniques | 1,5 | 17,20 | 17,00 | |
| Final Result | Engineer Professional Skills | 8 | 15,52 | 15,65 | C |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-LAB2411 | Robotics project and digital techniques | 2 | 13,71 | 13,7 | |
| ES-PHY2021 | Waves and propagation | 4 | 12,09 | 9,00 | |
| ES-SYS2041 | Digital electronics | 3 | 13,77 | 12,9 | |
| Final Result | Physics, Electronics and Systems | 9 | 12,96 | 11,34 | D |
Semester 2 — average 14.87/20, 30/30 ECTS
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF2032 | Operational research algorithms | 3,5 | 12,55 | 14,00 | |
| ES-INF2036 | (title as in source: ES-INF2036) | 1,5 | 14,44 | 16,25 | |
| ES-MAT2053 | Mathematics and Cryptography | 3 | 13,15 | 16,00 | |
| ES-MAT2054 | Series, approximation and probabilities | 3 | 11,68 | 15,5 | |
| Final Result | Mathematics and Computer Science | 11 | 12,73 | 15,26 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-ENT2112 | Career skills | 1 | 18,38 | 20,00 | |
| ES-HUM2093 | Personal Development Project | 2 | 15,65 | 17,00 | |
| ES-LAN2082 | English and TOEIC | 2,5 | 13,13 | 15,75 | |
| ES-TEX2066 | Oral expression techniques | 1,5 | 16,87 | 17,25 | |
| Final Result | Engineer Professional Skills | 7 | 15,80 | 17,04 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-ENT2113 | Discovering the World of Work | 1 | 16,35 | 19,00 | |
| Final Result | Block: Internship – Discovering the World of Work | 1 | 16,57 | 19,00 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-LAB2415 | Analog Electronics Project | 1,5 | 14,30 | 15,00 | |
| ES-PHY2022 | Signal generation and transmission | 4 | 12,21 | 11,5 | |
| ES-PLU2193 | Scientific and Technical Project | 3 | 14,22 | 15,5 | |
| ES-SYS2044 | Systems | 2,5 | 12,33 | 10,00 | |
| Final Result | Physics, Electronics and Systems | 11 | 13,07 | 12,73 | D |
Final average for the year: 14.19/20.
Year 3 (2022–2023)
The first semester was my exchange at Centria in Finland, 30 ECTS validated, detailed in its own entry. The second semester, back in Laval, was the first one almost entirely about computer science, and it shows: 18/20 in web programming, 17.25 in Python.
Semester 2 — average 14.43/20, 30/30 ECTS
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF3032 | Python as an engineering tool | 2 | 12,54 | 17,25 | |
| ES-INF3040 | System administration & CyberSecurity | 2 | 13,60 | 13,5 | |
| ES-INF3042 | WEB Programming | 2 | 11,98 | 18,00 | |
| ES-INF3043 | Algorithmics, computability and complexity theory | 2 | 13,35 | 14,00 | |
| ES-INF3044 | Mobile programming | 2 | 11,64 | 16,00 | |
| ES-INF3133 | Tools for Software development | 2 | 13,00 | 15,02 | |
| Final Result | Computer Science | 12 | 12,68 | 15,63 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-ENT3114 | Career skills seminar | 1 | 17,05 | 19,00 | |
| ES-ENT3116 | Introduction to Ethics | 1 | 9,87 | 13,5 | |
| ES-LAN3082 | English and TOEIC | 2 | 13,20 | 15,5 | |
| Final Result | Engineer Professional Skills | 4 | 13,71 | 15,88 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-HUM3093 | Personal Development Project | 2 | 12,97 | 14,5 | |
| ES-PLU3193 | Scientific and technical project | 3 | 13,86 | 15,00 | |
| ES-PLU3196 | Asserting oneself | 1 | 16,07 | 15,5 | |
| Final Result | Projects | 6 | 13,93 | 14,92 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-PHY3025 | From atom to quantum computing | 2 | 10,34 | 9,9 | |
| ES-SYS3042 | Automatic control systems | 3 | 12,77 | 12,1 | |
| ES-SYS3046 | Microprocessor: peripherals and applications | 3 | 13,05 | 12,1 | |
| Final Result | Physics, Electronics and Systems | 8 | 12,27 | 11,55 | D |
Final average for the year: 14.40/20.
Year 4 (2023–2024)
The software engineering major. The first semester was honestly rough: 9.44 in application design, 8.57 in data analysis, and my only D-graded blocks outside of physics. Then everything clicked in the second semester, 16.07 average, my best of the five years, with an A in the software engineering block and 18.15 in the project block.
Semester 1 — average 13.27/20, 30/30 ECTS
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-PLU4001 | Challenges and certifications | 1 | 19,20 | 20,00 | |
| ES-PLU4190 | Project in Digital Science and Technology | 7 | 15,63 | 17,00 | |
| Final Result | Core Program Project | 8 | 16,07 | 17,38 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-ENT4117 | Business game | 1 | 14,21 | 15,00 | |
| ES-ENT4305 | Internship Preparation | 1 | 16,65 | 18,00 | |
| ES-HUM4041 | Human impact on its environment | 0,5 | 10,21 | 10,72 | |
| ES-LAN4081 | English | 0,5 | 13,54 | 15,6 | |
| Final Result | Core Program – Corporate Culture | 3 | 14,17 | 15,39 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF4062 | Full-stack development | 3,5 | 13,05 | 11,7 | |
| ES-INF4066 | Application design | 2,5 | 12,23 | 9,44 | |
| Final Result | Software Engineering S7 | 6 | 12,71 | 10,76 | D |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF4032 | Computer networks | 3 | 12,94 | 10,72 | |
| ES-INF4033 | System programming | 2 | 12,82 | 11,5 | |
| ES-INF4046 | Applied Cryptography | 1 | 8,73 | 10,75 | |
| ES-INF4052 | Virtualization and containerization | 2 | 12,09 | 11,4 | |
| ES-INF4103 | Information Systems architecture | 1 | 15,72 | 17,4 | |
| ES-MAT4052 | Numerical in Python | 2 | 11,62 | 12,5 | |
| ES-MAT4056 | Estimation and Data Analysis | 2 | 10,16 | 8,57 | |
| Final Result | Core Program – Science and Technology | 13 | 12,07 | 11,4 | D |
Semester 2 — average 16.07/20, 30/30 ECTS
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-INF4051 | Application architecture | 3,5 | 12,17 | 16,3 | |
| ES-INF4063 | Software development using DevOps | 2,5 | 16,00 | 16,00 | |
| Final Result | Software Engineering S8 | 6 | 13,77 | 16,18 | A |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-PLU4002 | Challenges and certifications | 1 | 19,60 | 20,00 | |
| ES-PLU4191 | Project in Digital Science and Technology | 12 | 15,96 | 18,00 | |
| Final Result | Project & Enterprise | 13 | 16,37 | 18,15 | B |
| Code | Course | Coeff | Class Avg | Student | ECTS |
|---|---|---|---|---|---|
| ES-ENT4120 | Sustainable Business, CSR and Digital Sobriety | 2 | 16,29 | 17,5 | |
| ES-ENT4302 | Seminar | 1 | V | — | |
| ES-ICO4108 | Machine Learning | 3,5 | 12,40 | 9,85 | |
| ES-INF4064 | NoSQL | 2,5 | 13,49 | 12,59 | |
| ES-INF4065 | GPU programming | 2 | 17,62 | 17,22 | |
| Final Result | Core Program | 11 | 14,50 | 13,54 | D |
Final average for the year: 14.67/20.
Year 5 (2024–2025)
No ESIEA courses this year: the first semester was my exchange at Mid Sweden University (its own entry), and the second was replaced by my six-month end-of-studies internship at DNB: graded 92/100, which became my graduation grade.
One constant through the years: a D in every physics block, every semester it existed.
- JavaScript
- Python
- C
- Full Stack Development
- Application Design
- Algorithms
- Systems Programming
- Distributed Systems
Exchange Semester in Sundsvall, Sweden. Applying systems concepts both on paper and on the ski slopes. It was more project based, with real implementation of distributed systems and IoT. But also heavy on algorithm theory.
Five courses over the semester, graded on the Swedish scale from A (excellent) to F (fail).
| Code | Course | Coeff | Class Avg | Student Grade | ECTS |
|---|---|---|---|---|---|
| — | Swedish Language, Swedish for Foreign Students I | N/A | N/A | B | 7,5 |
| — | Distributed Systems and Computing | N/A | N/A | A | 6,0 |
| — | Distributed Algorithms | N/A | N/A | D | 6,0 |
| — | Advanced Networking Concepts | N/A | N/A | C | 6,0 |
| — | Implementing Internet of Things Protocols | N/A | N/A | B | 6,0 |
| Final Result | Erasmus Exchange – Mid Sweden University | — | — | — | 31,5 |
The pattern says a lot about me: an A in the hands-on distributed systems course, and the grade dropping as the theory density increases, bottoming out at D for distributed algorithms (for my own defense, we were 4 students and everyone got terrible grades).
And Swedish: B. Jag kan talar lite svenska nu.
- Algorithms
- Distributed Systems
- Advanced Networking
- IoT
Exchange Semester in Kokkola, Finland. Enjoyed the Northern Lights and the snow (and sometimes studied).
Got an introduction to databases (SELECT * FROM grades;), operating systems (playing with Arch) and different programming languages to do the same things (calculator app in Python and C#).
- Python
- C#
- SQL
- Operating Systems
- Object-Oriented Modeling
Projects
A chatbot that knows everything about me: my experiences, projects, career, all of it. Users can just ask anything and get real answers. It works as a conversational portfolio. I'm using React Server Components with the AI SDK, which is genuinely hard to pull off since their docs are not even working (and not even recommended anymore), but it makes the streaming flow feel really smooth. Built with Next.js, Tailwind, Drizzle, and the Vercel AI SDK.
That's one of my best projects, even if it's a bit old and I could do it much better now with the tech that came out since, but for that time, I've built a chat app where I give the system prompt the full context about me, and the LLM could answer users any questions about my career.
So I basically dumped my portfolio into an LLM that users can query. No login, but I store all conversations with Drizzle. I also use Vercel geo to give it to the LLM, so it can provide personalized answers.
I used the AI SDK, with RSC instead of UI, which is now deprecated because they couldn't make it work well, but I still did. They had some issues with flickering and stuff, but I found some ways and other tutorials, so everything runs on the server.
Full documentation for people to make it their own. Very nice UI, animations...
2 years after this project, I still haven't seen anyone build something similar. I'm trully a visionnary.
- TypeScript
- React
- Next.js
- Tailwind CSS
- AI SDK
- Drizzle
A place where I save links to UI libraries, shadcn registries, and websites that just look really good. When I need inspiration, I open a few of these and let my creativity do the rest. The UI is heavily inspired by zed.dev. Built with Next.js, Tailwind, Drizzle, and SQLite (Turso (because their free tier is really generous)).
- TypeScript
- React
- Next.js
- Tailwind CSS
- Drizzle
I initially followed the Youtube tutorial from the famous Code With Antonio to build a Notion clone with Next.js, Convex, shadcn/ui, Clerk. I then adapted it to be more focused on raw markdown for the editor part, with a separate preview. This was my first real polished application, with great UI, great features like real-time DB, soft delete with recovery, publish your documents to the web, file upload, export to PDF, lots of small UX details, landing page...
- TypeScript
- React
- Next.js
- Tailwind CSS
- Convex
- Clerk
A theme for the Zed Editor based on the colors from Vercel's docs. Currently top 30 most downloaded themes with 92k+ downloads. Lee Robinson was using it, which is pretty cool.
I've used Zed as my primary editor since the start now, I love it. So back then I converted the VSCode theme of Vercel to Zed using their theme importer, tweaked a few colors, and published it. That's my most popular repo, and I have 3 contributors that help me to refine it. And it has 93k downloads.
- Zed
- VIM



