# Nathan Brodin

> A frontend engineer with a passion for web development, design, and user experience.

I'm a frontend-leaning full-stack engineer. Originally from the west of France, I've been moving north ever since: internships in Oslo, exchange semesters in Finland and Sweden, and now settled north of the Arctic Circle in Tromsø, Norway.

I build things for the web that are fast, accessible, documented, and polished.

---

## Social Links

- [GitHub](https://github.com/NathanBrodin)
- [LinkedIn](https://linkedin.com/in/nathan-brodin)
- [Twitter](https://twitter.com/nathan_brodin)
- [Email](mailto:nathan@brodin.dev)

---

## Tech Stack

- **Languages**: TypeScript, JavaScript, Python
- **Frontend**: React, TanStack, Next.js, Tailwind CSS, shadcn/ui, Base UI, Vite, Redux, Playwright
- **Backend**: Node.js, Django, Convex, Drizzle, OpenAPI
- **Databases**: PostgreSQL, SQLite, ClickHouse, Redis
- **Infrastructure**: Linux, Docker, AWS, Vercel, OpenTelemetry
- **Workflow**: Git, GitHub, VIM, Zed, OpenCode, Figma

---

## Experiences

### [Capia AS](https://capia.no/)

#### Full Stack Engineer | 2025-08 - Present

Full-stack engineer at a small Norwegian data analytics company.

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](https://tanstack.com/), 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](https://playwright.dev/) E2E and checks on every PR
- UX: polished interfaces where every state is designed, refined through tight feedback loops with users
- Observability: [OpenTelemetry](https://opentelemetry.io/) across 8 repos into self-hosted [SigNoz](https://signoz.io/), 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: 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](https://www.docker.com/) 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](https://clickhouse.com/). 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 instructions I received were: build the backend with [Django](https://www.djangoproject.com/), the frontend with [React](https://react.dev/), use [Keycloak](https://www.keycloak.org/) 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, [PostgreSQL](https://www.postgresql.org/) for the app, Redis for caching, the external ClickHouse for organization data, Ruff + Pytest.
- **Frontend**: React 19 + Vite + [TypeScript](https://www.typescriptlang.org/), TanStack Router (file-based, type-safe routes), TanStack Form/Table, [Tailwind CSS](https://tailwindcss.com/) v4 + shadcn/ui, Recharts for charts, 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 Keycloak 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/ui, 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, access group, register, 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 --wait` so 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, organization 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 via `navigator.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 CSS 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.
- `matchMedia` for the mobile hook; `ResizeObserver` + `MutationObserver` to redraw a canvas decoration; `navigator.clipboard`; `usehooks-ts` for debounce and resize where it earns its place.
- React 19 + React Compiler enabled globally, with TanStack Router preloading routes on intent. `useTransition` is used for filter/table navigation and dialog submissions, route guards fetch data before rendering instead of `useEffect` fetching, 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**

- **Query 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.
- **Query 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 `setQueryData` from 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-context` handles 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 PostgreSQL (auth/users), an internal PostgreSQL (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 CSS v4 + shadcn/ui, Recharts for charts, MapLibre for the routes map); 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: PostgreSQL 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 + PostgreSQL, 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, `IntersectionObserver` for infinite scroll, `ResizeObserver` for table filler height and tree sizing, `MutationObserver` + `matchMedia` + `getComputedStyle` to 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-query` is 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 customer, 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 CI pipelines 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 GitHub organization level so every repo in the GitHub 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:

1. 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.
2. Management started giving away accesses to the different services we've been building, but we didn't know 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 set up a self-hosted SigNoz on the server to collect and view all data, and set up 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.

*Skills: TypeScript, React, TanStack, Tailwind CSS, Django, PostgreSQL, ClickHouse, Docker, Nginx, Playwright, OpenTelemetry*

### [DNB](https://dnb.no)

#### Frontend Engineer Intern | 2025-02 - 2025-07

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+ features across 3 frontend applications
- Migrated a production app from Gatsby to [Vite](https://vite.dev/), cutting build times by 60%
- Built a full E2E test suite with [Playwright](https://playwright.dev/). 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 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](https://react.dev/) 15, deps not updated since, install failing, no valid docs, 30s+ startup with useless logs, [TypeScript](https://www.typescriptlang.org/)/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+ `useEffect`s, 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](https://eufemia.dnb.no/) 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 mentioned was having a 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 renowned 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 UX, better than original, shipped to production to ~10 internal users, stakeholders happy.

One month in I already had 2 features in production, 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 assistant 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 launch 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 a 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's degree, as it replaced an entire semester, it got graded 92/100 for my work, presentation and report. Which for French schools is an extremely good grade.

*Skills: TypeScript, React, Redux, CSS, AWS, Playwright*

#### Frontend Engineer Intern | 2024-04 - 2024-08

Built the frontend of a GenAI chatbot platform used by 100+ users. From an empty repo to a well-tested app (92% coverage, [Storybook](https://storybook.js.org/), 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](https://www.typescriptlang.org/) (Vite), and [Redux](https://redux.js.org/) with RTK Query for data fetching, Storybook to render components in isolation with mocks, and [Playwright](https://playwright.dev/) for E2E testing.

Once again it was collaboration with the same 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 until `JSON.parse` succeeded, and appending content progressively. Gated behind feature flags with a type-safe `useFlag` hook 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 `/theme` page 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 with `api.util.updateQueryData` to patch the cached config from the frontend.
- Smaller stuff: chat history took 2-3 hours to implement as the backend engineer 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 documentation 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.

*Skills: TypeScript, React, Redux, CSS, Storybook, Playwright*

#### Frontend Engineer Intern | 2023-07 - 2023-08

Shipped an internal admin panel from scratch used by 10+ users, based on Figma designs. Small project with basic tech (CRA + [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)), 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 designer of the team, that had all designs 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, styled-components for styling, [Redux](https://redux.js.org/) for state, Axios for API calls. It was my first "real" [React](https://react.dev/) 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.

*Skills: JavaScript, React, Redux, CSS, styled-components, React Router, AWS*

### Education

#### ESIEA Graduate School of Engineering | 2020-09 - 2025-07

My Master's in Software Engineering, from [ESIEA](https://esiea.fr), a top 10 French engineering school and the 2nd best private one for computer science ([L'Étudiant](https://www.esiea.fr/les-classements-de-2024-sont-tombes/)).

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       | Programming project                     | 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](https://www.python.org/).

**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.

*Skills: JavaScript, Python, C, Algorithms, Systems Programming, Distributed Systems, Full Stack Development, Application Design*

#### Mid Sweden University | 2024-09 - 2025-01

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 tala lite svenska nu.

*Skills: Algorithms, Distributed Systems, IoT, Advanced Networking*

#### Centria University of Applied Sciences | 2022-09 - 2022-12

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 Linux) and different programming languages to do the same things (calculator app in [Python](https://www.python.org/) and [C#](https://learn.microsoft.com/en-us/dotnet/csharp/)).

*Skills: Python, C#, SQL, Databases, Operating Systems*

---

## Projects

### [Chat](https://chat.brodin.dev) | 2024-07 - 2025-03

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](https://react.dev/) Server Components with the [AI SDK](https://ai-sdk.dev/), 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](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/), [Drizzle](https://orm.drizzle.team/), 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 geolocation 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 truly a visionary.

*Skills: TypeScript, React, Next.js, Tailwind CSS, AI SDK, Drizzle*

### [Bookmarks](https://bookmarks.brodin.dev) | 2025-09

A place where I save links to UI libraries, shadcn/ui 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](https://zed.dev). Built with [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/), [Drizzle](https://orm.drizzle.team/), and SQLite (Turso (because their free tier is really generous)).

*Skills: TypeScript, React, Next.js, Tailwind CSS, Drizzle, SQLite*

### [Write](https://write.brodin.dev) | 2024-02 - 2024-03

A Notion-style Markdown editor. Write and preview Markdown in real-time, export to PDF. Started as a Notion clone from a tutorial, and then adapted to my needs for raw Markdown editing. Built with [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) and [Convex](https://www.convex.dev/).

I initially followed the YouTube tutorial from the famous Code With Antonio to build a Notion clone with Next.js, Convex, shadcn/ui, [Clerk](https://clerk.com/). 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...

*Skills: TypeScript, React, Next.js, Tailwind CSS, Convex*

### [Zed Vercel Theme](https://zed.dev/extensions?query=vercel) | 2024-10 - 2025-03

A theme for the [Zed Editor](https://zed.dev) based on the colors from Vercel's docs. Currently top 30 most downloaded themes with 94k+ downloads. [Lee Robinson](https://x.com/leerob/status/1870561189585101288) 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 VS Code 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.

*Skills: Zed*

### [Open Source Contributions](https://github.com/search?q=is%3Apr+author%3ANathanBrodin+is%3Apublic+-user%3ANathanBrodin+-repo%3Azed-industries%2Fextensions&type=pullrequests) | 2022-08 - Present

Small fixes here and there whenever I run into something broken. A couple 2-line UI fixes in [coss ui](https://coss.com/ui) (and I'm not even kidding), a tooltip fix in Eufemia, and docs improvements in [Zed](https://zed.dev) and [Supabase](https://supabase.com). Quality over quantity, I guess...?

*Skills: GitHub*

### [Trends](https://github.com/NathanBrodin/trends) | 2026-06 - Present

That's a project that has been going on and off for years, with no output still. I wanted to create my own personal finance application, which is focused on the future by using the current and past income/expenses to see how my finances will evolve in the future.

I started it in 2023 I think, with a Next.js app, with a Zed UI (previous to my UI lib, which is a big part of the reason why I created it), but couldn't be satisfied by it.

Then I tried again in 2025 but stopped again, then trying again since June 2026, I first had to create the Template to have a strong foundation, then work on it, but the code was starting to be messy for my standards, at that time you had to manually enter the data, which gave great results, but it would not be usable as the data would drift from reality the day after you entered the data, and I made a complete overview page with lots of charts, spent lots of time to make it really really nice, but realized that the information you get out of it was not that valuable.

So I refactored it with a monorepo structure to keep everything perfect using the better-t-stack template, and decided to rebuild it entirely from the ground up to fetch the data directly from my accounts, using the "Enable Banking" PSD2 provider, but after a month building on it, realized that the data is unusable, like transactions are not categorized and contain little information, and the worst was that you have to first connect your accounts to the Enable Banking admin dashboard and then you could use it in my app, but this means I can never publish the app.

Then I switched to the GoCardless API, which is a bit better, but I kinda lost motivation as the data is still missing lots of information.

*Skills: TypeScript, React, TanStack, Next.js, Tailwind CSS, Convex*

### [Templates](https://github.com/NathanBrodin/template) | 2026-04 - 2026-06

I made a monorepo template with [TanStack Start](https://tanstack.com/start/latest) for the web, [Convex](https://www.convex.dev/) for the backend, a UI package, and table and form packages using the new TanStack composable patterns, [Better Auth](https://www.better-auth.com/), marketing and legal pages being pre-rendered, i18n, Polar.
I wanted to make that as every time I started a new project, I had to re-setup everything, re-handle auth, and stuff.

So it comes with email/password and Google/GitHub OAuth, email verification, password reset, a full account settings section (avatar upload with cropping, link/unlink social accounts, password change, account deletion), app-wide i18n with localized routes, landing/FAQ/pricing pages, localized Markdown-driven legal documents (privacy, terms, cookies), and the full SEO package: per-page metadata, JSON-LD, dynamic OG image generation, sitemap. Everything before the product, done once, done right.

*Skills: TypeScript, React, TanStack, Tailwind CSS, Convex, SEO*

### [Portfolio](https://brodin.dev) | 2026-02 - Present

My newest portfolio, built with [TanStack Start](https://tanstack.com/start/latest) (static pre-rendering) and Content Collections for content in Markdown, with a borderline-obsessive focus on performance, accessibility, and SEO.

I finally moved away from Next.js and went with TanStack Start instead. Also using coss ui over shadcn/ui, it looks so nice.

Full SEO (JSON-LD, sitemap, OG image generation, llms.txt, RSS feed), sound design, and lots of tiny details in the UX as well as in the code. I've spent a lot of time working on performance, accessibility and SEO.

Accessibility means a 100 Lighthouse score, but also a fully cleaned accessibility tree which is entirely readable and free of decoration noise. Icon buttons with descriptive tooltips, contrasts, element focus, and everything else.

Performance means a 100 Lighthouse score on desktop, but also ~95 on slow mobile 4G which is the one that matters for Google. It also means optimized font loading, image loading and formats, GPU-friendly animations. Powered with static pre-rendering, which still correctly renders the first paint with the correct theme and the correct keyboard shortcut key (⌘/Ctrl) with no layout shift. The dotted world map on the homepage is even precomputed at build time to avoid ~1000ms of main-thread work.

SEO means a 100 Lighthouse score, but also fully defined meta tags, with per-route OG image generation (Puppeteer screenshots), canonical URLs and Twitter cards. It also means Structured Data (JSON-LD) across key routes, including Person, Blog and BlogPosting schemas, plus a sitemap and robots.txt. SEO also means GEO (Generative Engine Optimization), with an llms.txt and llms-full.txt, every blog post readable as raw Markdown at `/blog/<slug>.md`, and a skill.md for AI agents.

*Skills: TypeScript, React, TanStack, Tailwind CSS, SEO*

### [Films](https://github.com/NathanBrodin/films) | 2025-09

Unfinished, but this was supposed to be an app where I could log all the snowboard/ski movies I've seen, so Letterboxd for documentaries, with the UI from my component library. [Next.js](https://nextjs.org/), [Drizzle](https://orm.drizzle.team/)...

*Skills: TypeScript, React, Next.js, Tailwind CSS, Drizzle*

### [UI](https://ui.brodin.dev) | 2025-06 - 2025-09

My own component library. It was created before [shadcn/ui](https://ui.shadcn.com/) released a tool to create themes directly, and before the migration to Base UI, so I abandoned it.

After bookmarks, I realized that making my own shadcn/ui registry would help me in my other projects, with it looking like Zed. So that's what I did. This was very early for shadcn/ui registries, so I forked shadcn/ui to build my own documentation site and adapt everything to my likings. So a [Next.js](https://nextjs.org/) app once again.

*Skills: TypeScript, React, Next.js, Tailwind CSS, shadcn/ui, Base UI*

### [Repos](https://github.com/NathanBrodin/repos) | 2025-04

During the interview process for Capia, I got tasked to create a small project over the weekend: a simple [React](https://react.dev/) app with [Redux Toolkit](https://redux-toolkit.js.org/) to fetch the list of GitHub repos for a given username. Simple stuff.

So I recreated the UI of the GitHub profile for the list, with filtering and sorting, loading and error states, debounce, clean code and documented... Built with [Vite](https://vite.dev/), [Tailwind CSS](https://tailwindcss.com/) and [shadcn/ui](https://ui.shadcn.com/). I got the job.

*Skills: TypeScript, React, Redux, Tailwind CSS, shadcn/ui*

### [Ice Calendar](https://github.com/NathanBrodin/ice-cal) | 2024-11

I made a quick [Next.js](https://nextjs.org/) app to scrape the calendar of the public openings of Sundsvall's ice rink, as I would go ice skating 4 times a week. So it gets the raw HTML from the old website, formats it in cal format, and allows you to set it up, and choose what kind of openings you're interested in using URL search params.

Nice and simple UI, with an aurora effect.

*Skills: TypeScript, React, Next.js, Tailwind CSS*

### [IoT](https://github.com/NathanBrodin/IoT) | 2024-11 - 2025-01 · School project

During my exchange in Sweden I took the "Implementing Internet of Things Protocols" course, which was basically three big programming projects: an HTTP/REST server, a [CoAP](https://coap.technology/) client, and an [MQTT](https://mqtt.org/) broker, all written in [Rust](https://www.rust-lang.org/).

The rule of the course: everything from scratch, no external libraries allowed, so you actually understand the protocols.

The final project combined everything into one IoT system, with the broker and a CoAP server in [Docker](https://www.docker.com/), plus a small frontend to visualize the sensor data. Fully vibe coded, but it worked well (this means I've seen Rust but I don't know Rust).

*Skills: Rust, Docker*

### [Mail](https://github.com/NathanBrodin/Mail) | 2024-02

"Redefining Your Inbox Experience" is a mail client to rethink how you manage messages, prioritizing and responding with intuitive simplicity (that's what the README says at least).

[Next.js](https://nextjs.org/), [TypeScript](https://www.typescriptlang.org/), [Tailwind CSS](https://tailwindcss.com/) with [shadcn/ui](https://ui.shadcn.com/), auth with [NextAuth.js](https://next-auth.js.org/) via GitHub, a discover page and a proper new-user experience, full i18n in English and French, theme customization, [PostHog](https://posthog.com/) for analytics, [Sentry](https://sentry.io/) for error tracking, and [Cypress](https://www.cypress.io/) E2E tests running in CI, with a stub login so the tests could get past the auth.

It was live at mail.brodin.dev. I had just started the Gmail integration when I dropped it... and started Write the very next day. So I built everything around the product but the product, lol.

*Skills: TypeScript, React, Next.js, Tailwind CSS, shadcn/ui*

### [Interactive map](https://gitlab.esiea.fr/brodin/interactive-map) | 2024-01 - 2024-02 · School project

For the Software Development using DevOps class, I made an app that lets you look for cities around you: you click on the map to search, change the search filters in a side panel, and click on a city to see more details about it.

All on [Docker](https://www.docker.com/) (one `docker compose up` and it runs), with CI/CD, docs with architecture diagrams, great docs. Client with [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/), and [Cypress](https://www.cypress.io/) for E2E testing. Backend with [ASP.NET](https://dotnet.microsoft.com/apps/aspnet) Core, [C#](https://learn.microsoft.com/en-us/dotnet/csharp/) and xUnit for unit tests, and [PostgreSQL](https://www.postgresql.org/) database. OpenAPI specs, and great code coverage, with the pipeline and coverage badges directly in the README.

So this was a full stack app with all of the great software engineering practices.

*Skills: TypeScript, C#, React, Next.js, Tailwind CSS, ASP.NET, PostgreSQL, Docker*

### [B-moveOn](https://gitlab.esiea.fr/brodin/B-moveOn) | 2023-09 - 2024-04 · School project

I built a mobile application in [Flutter](https://flutter.dev/) for delivery drivers on cargo bike, allowing them to input all their delivery points, with the app calculating the fastest route.

---

For the 1-year project of my 4th year. We built a full project for an external company, a Spanish company that produces cargo bikes, and they wanted an app for delivery drivers where they could enter all of the places they need to go, and the app would show the best path, and direct them during the ride.

So at the school we had the Spanish teacher as our mentor, who would connect us with the CEO of that company, and we also collaborated a lot with the local commune as it was a good use case for them. They basically had a few employees that would have to go to multiple compost places during some days of the week. This project was with two others, but for the code itself they didn't do anything, lazy af, so I built everything, I also had to prepare the presentations, reports... at least they came with me when we had the meetings and stuff.

With the local commune I remember we had a few workshops first to see their needs and how our app could help them, and on the last week we went with them on a tour to see if it was actually working, so we biked across the city with them, I remember the mentor following us in the car with one member of our team filming us biking so we could make a nice video.

The project started already the year before, and we were given the codebase of that group. To my surprise this was terrible, they basically did nothing, auth was fake, and nothing was working, so I rebuilt everything. Of course the mentor and CEO were non-technical, so they had the craziest requests to give to unpaid students, so this was a tough challenge to try to explain and defend stuff, like that I couldn't build on the previous foundations, explaining that things take time... Project management is something...

So for the non-technical part of project development, this was a very valuable time, as I got an excerpt into the real corporate world.

The app was Flutter with [Supabase](https://supabase.com/) as the backend and [Riverpod](https://riverpod.dev/) for state management. I started with the Google Maps API to trace the journey and calculate paths, but switched to [OpenStreetMap](https://www.openstreetmap.org/) during development. I also added a weather widget for the ride, and made the app available in French, English and Spanish (the CEO is Spanish after all). Made the UI first in [Figma](https://www.figma.com/), looking really nice as always, and implemented it. The logo, it was a bike wheel, but with the colors of the maps journey, with the maps arrow, so it was a combination of the map and a wheel, genius.

Also deployed on the [Play Store](https://play.google.com/store/apps/details?id=fr.esiea.bmoveon) and [App Store](https://apps.apple.com/us/app/b-moveon/id6471257425), with CI/CD pipelines ([Fastlane](https://fastlane.tools/)) to build and push releases automatically. Huge documentation, even some integration tests running on an Android emulator in the pipeline.

At the end of the year we had the project's fair, which we finished second once again, but this time there were two prizes: Jury and Public, second at both. But the local newspapers came to interview our project, and we got our own [article in the newspaper](https://www.ouest-france.fr/pays-de-la-loire/laval-53000/ces-etudiants-ingenieurs-de-laval-developpent-les-applications-du-quotidien-de-demain-7dc431b4-f281-11ee-bc38-55f66082c1a5)

*Skills: Dart, Flutter, Riverpod, Supabase, OpenStreetMap, Fastlane, Figma*

### [Portfolio V3](https://github.com/NathanBrodin/nathanbrodin.com) | 2023-07 - 2023-10

I rebuilt my portfolio once again, as I got heavily inspired by Chronark's portfolio. But it wasn't a fork, I rebuilt everything to follow my code style, updated the UI to match my style as well.

I also spent a lot of time fighting performance issues, lazy loading components and dynamic imports until the Lighthouse score made me happy, and plugged in Vercel Speed Insights to keep an eye on it.

*Skills: TypeScript, React, Next.js, Tailwind CSS*

### [Chat App](https://gitlab.esiea.fr/brodin/chat-app) | 2023-05 · School project

For school we had to make a chat app with [Node.js](https://nodejs.org/), as the end of semester project of our web class. I first made the UI on [Figma](https://www.figma.com/), then built the server, and the HTML/CSS.

The server is [Express](https://expressjs.com/) with [Socket.IO](https://socket.io/) for the real-time messaging, bcrypt for password hashing, sessions to stay logged in, and multer to upload images. The app has a global room, private messages by clicking on a user in the sidebar, profile pictures, and image sending. It even prevents you from logging in twice with the same account.

*Skills: JavaScript, HTML, CSS, Node.js, Express*

### [Pokedex](https://gitlab.esiea.fr/brodin/pokedex) | 2023-05 · School project

Small [Node.js](https://nodejs.org/) server ([Express](https://expressjs.com/)) and web app to display pokemons, for school.

*Skills: JavaScript, HTML, CSS, Node.js, Express*

### [Sokoban](https://gitlab.esiea.fr/brodin/sokoban) | 2023-05 - 2023-06 · School project

For school, I made the Sokoban game in [Flutter](https://flutter.dev/).

The game has three modes: continue your game (it saves every move, so you can quit and come back where you were), start a new game, or pick a level (only the ones you unlocked, of course). Boxes turn green when they're on the right spot, you can undo moves or restart, and scroll around to see the whole map.
I even added music and sound effects, and a custom app icon.

*Skills: Dart, Flutter*

### [Eco-friendly watering](https://gitlab.esiea.fr/brodin/arrosage-eco) | 2023-04 - 2023-06 · School project

For the 6-month project of my 3rd year of engineering school, I built a device to put on a plant that had sensors for temperature and humidity, running on an [STM32](https://www.st.com/en/microcontrollers-microprocessors/stm32-32-bit-arm-cortex-mcus.html) card. The card hosts a small web server written in C, and the [Flutter](https://flutter.dev/) app connects to it over Wi-Fi to fetch and display the data.

Made the sketches first on [Figma](https://www.figma.com/), with of course a nice UI, and then built it.

*Skills: C, Dart, Flutter*

### [docs.nathanbrodin.com](https://github.com/NathanBrodin/docs.nathanbrodin.com) | 2023-03 - 2023-06

I was somehow obsessed with resetting my Windows laptop every 6 months to remove the bloat (slow and cheap computer), and I was customizing it a lot every time. So I made a [Next.js](https://nextjs.org/) docs application with [Nextra](https://nextra.site/), where I wrote each step to do in [MDX](https://mdxjs.com/) docs.

So every default setting to change, all apps to install, tips... A full step-by-step walkthrough to get a clean and optimized Windows 11 installation, based on years of trial and error.

*Skills: TypeScript, React, Next.js, MDX*

### [Weather App](https://github.com/NathanBrodin/Weather-app) | 2023-03 · School project

For school we had to make a weather app in [Flutter](https://flutter.dev/). I went all in with the UI, used [Midjourney](https://www.midjourney.com/) to generate the background images, so that it would show a nice landscape based on the weather. It fetches data from the [OpenWeatherMap](https://openweathermap.org/) API: current conditions, temperature, wind speed, and a forecast for the next 5 days. Icons picked from a Dribbble shot, mockups made with shots.so.

I made nice mockups of the app, wrote some documentation, created the releases... This was quite far of the initial simple requirements of the teacher.

*Skills: Dart, Flutter*

### [Wordle](https://gitlab.esiea.fr/brodin/wordle) | 2023-03 · School project

We had to make a Wordle app with [Flutter](https://flutter.dev/) for the Mobile programming course. Quite simple and fits the teacher's requirements.

*Skills: Dart, Flutter*

### [Grammar Checker](https://github.com/NathanBrodin/grammar-checker) | 2022-10

At the very start of [OpenAI](https://openai.com/) and LLMs, I made a grammar checker with a very similar UI to the Quillbot Grammar Checker, and made the logic by having a simple prompt to GPT-3 to correct the text I'm passing. Seeing how AI is at the center of the world now, it's fun to see I made this project in Autumn 2022, before ChatGPT even existed.

Once again with [Next.js](https://nextjs.org/) and [Tailwind CSS](https://tailwindcss.com/), fully responsive, with dark and light mode, a copy-to-clipboard button, and an undo button for the last correction.

This was my first "polished" web application, which had nice details and completed. For some reason it became "popular", with 16 stars and 4 forks on GitHub.

*Skills: JavaScript, React, Next.js, Tailwind CSS*

### [Portfolio V2](https://github.com/NathanBrodin/nathanbrodin.com) | 2022-09 - 2022-11

Rebuilt my portfolio with [Next.js](https://nextjs.org/) and [Tailwind CSS](https://tailwindcss.com/). Better UI, better designs, but you can still see that I was a "beginner". I even made it fully available in French, and added Google Analytics to see if anyone was visiting (they were not).

This might have been my first Next.js application, which came right after my first [React](https://react.dev/) project. Back in 2022 Vercel was at the top of DX and good documentation.

*Skills: TypeScript, React, Next.js, Tailwind CSS*

### [MyPasswordManager](https://github.com/NathanBrodin/MyPasswordManager) | 2022-06 - 2022-09

I made a [Chrome extension](https://developer.chrome.com/docs/extensions) to store and autofill passwords, with [React](https://react.dev/), which is uncommon for extensions, but I found a template (create-react-extension), and stored the data in [Firebase](https://firebase.google.com/) Firestore.

Everything is encrypted with AES (CryptoJS) before being sent to Firestore, and the secret key is generated and stored locally on your computer, so nobody else can read your data. It could also auto-submit the login form for you if you enabled it.

Made a nice UI for the extension, and even published it on the Chrome Web Store.

I made it while in Finland for my exchange semester, I remember forcing my friends to review it 5 stars on the Chrome Web Store.

*Skills: JavaScript, React, Firebase*

### [Portfolio V1](https://github.com/NathanBrodin/nathanbrodin.github.io) | 2022-03 - 2022-05

After we had a workshop at school to create our CV and LinkedIn, the teacher mentioned portfolios, so I was interested to make mine. This was also after I started to work on the ESIEABOT mobile app, so I discovered that I liked building UIs (even if I already loved making fancy PowerPoints and other designs).

So I first designed it on [Figma](https://www.figma.com/), then wrote my first lines of HTML, CSS and a little of [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) for some small interactions, and deployed it on GitHub Pages.

*Skills: HTML, CSS, JavaScript*

### [ESIEABOT Mobile App](https://github.com/NathanBrodin/Esieabot-Mobile-App) | 2021-10 - 2022-06 · School project

An [Android](https://developer.android.com/) mobile app to control remotely the ESIEABOT (a robot with two wheels and a camera). I made a tiny [Raspberry Pi](https://www.raspberrypi.com/) image to put on the bot, which listens to Bluetooth, can receive directions, and sends the video feedback. And I made the mobile app (in [Kotlin](https://kotlinlang.org/)) to send the controls, and see the video feedback. I made a nice UI, nice features to control and connect to the robot.

This was lots of work, but very good work.

It features control via 4 directional arrows, live camera view, a return-to-home function, ultrasonic sensor activation, a room scan function, a usage guide, automatic pairing with a new robot, and even configuring the robot's Wi-Fi directly from the app. I shipped releases on GitHub up to v1.4, with the APK and the OS image ready to flash.

This was the 6-month project we had to do during my second year of engineering school.

We were 3 on the project but unfortunately they did not do anything. We had 6 months to do it, they wanted to wait for the last moment like anyone else, but I started working from the very start and continuously, and tried to convince them, but it did not work. So I made all presentations, the codebase, and everything basically.

At the end of the year, we had a project's fair for all students, and we (I) finished second among the 20 projects.

*Skills: Kotlin, Android*

---

## Certifications

- [Apollo GraphQL - Graph Developer - Associate](https://www.apollographql.com/tutorials/certifications/7d67206d-3e7c-42cf-9ed2-768f3f3bc362) | 2024-06
- [Meta - Advanced React](https://www.coursera.org/account/accomplishments/verify/CUYRZ6XRFNGJ) | 2024-02
- [Meta - React Basics](https://www.coursera.org/account/accomplishments/records/RNXWP8EW8RBB) | 2024-01
- [Coursera - Mastering the Software Engineering Interview](https://www.coursera.org/account/accomplishments/certificate/RVCLPYR3YRQ7) | 2023-10

---

## Blog

> Articles on development, design and ideas.

- [OG Images Without an Image CDN](https://brodin.dev/blog/og-images-without-an-image-cdn.md) - I render Open Graph previews as real React routes and screenshot them with Puppeteer in a build script, so my link previews reuse the same components as the site. | 2026-07-10
- [Every Optimization Behind a Perfect Lighthouse Score](https://brodin.dev/blog/every-optimization-behind-a-perfect-lighthouse-score.md) - The 100 on Lighthouse isn't luck or a clean stack. Here is every optimization I actually shipped to get brodin.dev to 100 locally and 100 in Vercel Real Experience Score. | 2026-07-03
- [react-i18next Was Fine. Then I Found Paraglide.](https://brodin.dev/blog/paraglide-vs-react-i18n.md) - A comparison of react-i18next and Paraglide JS from someone who has shipped both in production: what I was missing, why I switched, and the honest downsides. | 2026-05-04
- [A Totally Legitimate Way to Look More Productive on GitHub](https://brodin.dev/blog/fake-github-contribution-graph.md) - A tutorial on how to fake your GitHub contribution graph. | 2026-04-17
- [Between Tradition and Modernity: Building a Full Stack App with Django and React](https://brodin.dev/blog/building-a-full-stack-app.md) - A deep dive into bridging the gap between isolated APIs, enforcing type safety across the stack, and finding the value in building robust architecture for small user bases. | 2026-03-16
- ["You Wouldn’t Steal a DIV": How I Built My Portfolio](https://brodin.dev/blog/how-i-built-my-portfolio.md) - A story about how I built my portfolio and what went through my mind while building it | 2026-03-04
