Fullstack Developer, Zürich

Alex Grädel

Fullstack developer in Zürich. I started at 16 as an apprentice at Swisscom, consulted for a fintech and the federal intelligence service, then built the webshop frontend for a hosting company. Since 2024 I've been on Amazon's Lens team through Makeen, mostly React and TypeScript with Node and PostgreSQL behind it. That work is paused while I do my civil service. Outside work I keep a Mac mini at home running local LLMs, n8n and Paperless, and I look after the website and server for LustLaune, a rave collective.

Fullstack developer in Zürich, six years in after a four-year apprenticeship at Swisscom. On Amazon's Lens team through Makeen, paused for civil service. Before that: a hosting company's webshop, and consulting for a fintech and the federal intelligence service.

Most recent first

Makeen AG Senior Fullstack Developer

Consultancy. Placed with Amazon since I joined.

  • Consultancy. I've been placed with Amazon since I joined.

Amazon Fullstack Developer Paused for civil service

Lens team, joined before launch. Features end to end on AWS: WebSocket progress updates, server-side pagination, a geospatial risk check, Japanese localisation. Cypress E2E suite, React Router v7 migration.

On the Lens team, which builds the tool Amazon's real-estate teams use to evaluate and acquire new sites. I joined before launch; it's used worldwide now. React, TypeScript, Node and PostgreSQL on AWS.

  • Built features end to end, from the form down to the database: validation, server-side pagination, live progress over WebSockets.
  • Designed a picklist service that pulls from three sources (constants, PostgreSQL and Salesforce) behind one interface, with Redis caching and Zod validation.
  • Built a risk check that finds residential buildings near a site using OpenStreetMap data and shows them on an ArcGIS map.
  • Added Japanese localisation: address autocomplete down to prefecture, city and ward, tsubo as a unit, local number and date formats.
  • Worked on the Cypress E2E suite: moved it to TypeScript, added page helpers and wrote up how to make it faster. Also wrote a small Chrome DevTools Protocol profiler so performance regressions show up as numbers.
  • Helped with the move to React Router v7 and the React Compiler.

React, TypeScript, Node.js, AWS, PostgreSQL, Prisma, Cypress, React Router, Redis, tRPC, ArcGIS/ESRI, Zod, WebSockets, Salesforce, OpenStreetMap

Kreativmedia GmbH Frontend Developer

Swiss web hosting provider. Built the customer webshop frontend in Angular 17 with signals, planned its API, and worked on internal and customer-facing tools.

Swiss web hosting provider.

  • Built the frontend of the customer webshop in Angular 17 with signals.
  • Planned the webshop's API together with the backend.
  • Kept working on the internal tools and the self-service tools for customers.

Angular, Angular Material, Tailwind, SCSS, OpenAPI (Elements), Docker, GitHub Actions

Cleo AG Software Developer

Consultancy. Two clients: a fintech and the federal intelligence service.

  • Consultancy. Two clients in one year: a fintech and the federal intelligence service.

NU Technology AG Software Developer

Moved a React app to Next.js, built the automated onboarding for new customers, set up E2E tests and static analysis in GitHub Actions.

  • Moved the React app to Next.js.
  • Planned and built the sign-up flow that onboards new customers automatically.
  • Set up GitHub Actions for E2E tests and static analysis.

NextJS, React, Cypress, Playwright, Typescript, GitHub Actions

Nachrichtendienst des Bundes Software Developer

Web application for the Swiss Federal Intelligence Service. Details under NDA.

  • Worked on a web application for the Swiss Federal Intelligence Service. The details are under NDA.

NU Technology AG Software Developer

Turned the style guide into React components and integrated Keycloak for login.

  • Turned the style guide into React components and cleaned up the frontend along the way.
  • Integrated Keycloak for login.

React, Typescript, Tailwind, Cypress, GitHub

Swisscom AG DevOps Engineer (Frontend)

Stayed on after the apprenticeship to keep working on Hubble, Swisscom's cloud billing tool, and trained the next apprentice.

Stayed on after the apprenticeship to keep working on Hubble, Swisscom's internal tool for cloud billing and reporting.

  • Kept building out the Hubble frontend.
  • Trained the next apprentice and handed the project over.

Angular, Angular Material, Typescript, SASS, Bitbucket

Swisscom AG / BBW Application Developer EFZ

Four-year apprenticeship at Swisscom, mostly on Hubble, the cloud billing frontend. Some backend work with Node.js, Spring Boot and Apigee.

Four-year apprenticeship as an application developer, with school at BBW. At Swisscom apprentices choose their own projects, so I moved through several teams instead of staying in one.

  • Worked on Hubble, the frontend for Swisscom's cloud billing and reporting. I stayed with it after graduating.
  • Integrated a management UI into the existing CI/CD setup.
  • Looked for things the DevOps team could automate and improved existing workflows.
  • Tested the build pipeline and wrote down what to improve.
  • Wrote and maintained the project documentation.
  • Backend work with Node.js, Spring Boot and Apigee; API mocks with Mountebank and CA LISA.

Angular, NodeJS, Typescript, Spring Boot, Git, GitHub, Docker

What I reach for

Frameworks

  • Angular
  • React
  • NextJS
  • Astro
  • React Router

Backend

  • Node.js
  • tRPC
  • Zod

Cloud & Database

  • AWS
  • PostgreSQL
  • Prisma
  • Redis

CMS

  • Strapi
  • Ghost

Styling

  • Tailwind
  • SCSS
  • Angular Material
  • UnoCSS

Languages

  • TypeScript
  • JavaScript

Testing

  • Cypress
  • Playwright

DevOps

  • Docker
  • GitHub Actions
  • n8n
  • Portainer
  • Bitbucket
  • GitHub

API

  • OpenAPI (Elements)
  1. Gradus gradus-self.vercel.app

    Open-source school workspace for Swiss schools, with an AI assistant that proposes and people approve

    • Angular 22 workspace (zoneless, Signals) over a FastAPI core API that is the single authority for roles, rules and data
    • Gradus AI on Swiss-hosted models through 37 MCP tools: every write becomes a proposal in a server-side ledger, shown as a diff, applied only when a person with the right role accepts it
    • Swiss specifics built in: the 1–6 grading scale, half-day absences, cantonal Zeugnis layouts, BFS learner statistics, payroll rounded to 5 Rappen
    • Docs site, OpenAPI and MCP reference generated from the code; deployed as one Vercel project with a Python and a Bun function

    Angular, TypeScript, Tailwind, Python, FastAPI, SQLite, Bun, MCP, Zod, Vitest, Biome, Vercel

  2. NULLPUNKT edugym-navy.vercel.app

    Cooperative maths adventure for the classroom, played on iPads and the projector

    • Three surfaces from one Angular 20 app: operator iPads, a teacher console, and a three.js station on the projector that reacts to every solved task
    • Server-authoritative rooms over WebSockets (Bun + Elysia): clients send intents, the game engine reduces them, and each audience gets its own filtered view
    • Framework-free maths engine with seeded exercises and Swiss-aware answer normalisation (4,46 / 4.46 / 1'273); CI plays the whole lesson in Playwright
    • Privacy by design: no pupil accounts, only first names and per-device tokens; Supabase Postgres in production

    Angular, TypeScript, Bun, Elysia, WebSockets, Three.js, Supabase, PostgreSQL, Drizzle, Playwright, Vitest, Vercel

  3. zeitiv.dev

    This site. Prints as a one-page CV.

    Astro, Angular, UnoCSS, TypeScript

  4. lustlaune.ch

    Website for LustLaune, a rave collective, with Three.js scenes. I also run the server it lives on.

    Astro, Tailwind, Three.js, TypeScript

  5. Lula Dice github.com

    Small dice-rolling app in Svelte.

    Svelte, TypeScript

A side project, opened up

Gradus is the school workspace I'm building for Swiss schools: Angular in front, FastAPI behind, and an assistant on Swiss-hosted models that can read what you can read but never changes a record by itself. It proposes. People approve.

Its docs are generated from the code, down to a live preview of almost every component. Two of those run right here, the real code pulled from Gradus's main branch and painted in this page's colours by its own theme engine. After them, the code I'm proudest of, step by step.

components, mapped from the source
77
of them render live in the docs
69
MCP tools for the assistant
38
test files, front to back
76

Components

62 source files vendored from Gradus, the rest captured from its docs

<app-proposal-panel> Intelligence Live, the real code

Palette

Loading Review ledger…

Everything the assistant wants to change waits here as a diff. Nothing is written until a person with the right role accepts it. Decide, and the ledger answers as the server would.

Inputs
proposals*decidingoverlay
Outputs
(decide)(decideAll)(dismiss)
Also live in here
ProposalCardJsonViewer
In the tree
Inside Chat Renders ProposalCard, Translate, UiIcon

Code tour

Pick a step, the lines light up

Backend · Python · FastAPI

The assistant proposes, people approve

The problem The AI must never change school data on its own, and the record can change between the moment it proposes something and the moment someone clicks Accept.

1 / 4

backend/approvals.py L121–160 GitHub ↗
@router.post('/proposals/{pid}/decision')
def decide(pid:str,data:Decision,x_ai_token:str=Header(),x_ai_session:str=Header(),user:dict=Depends(get_current_user)):
    """Accept or reject a pending proposal. Accepting executes the underlying change only if the target record hasn't changed since the proposal was staged; otherwise the proposal is marked Stale."""
    authorize(x_ai_token,'review')
    with connect() as db:
        db.execute('BEGIN IMMEDIATE')
        row=db.execute('SELECT * FROM proposals WHERE id=? AND session=?',(pid,x_ai_session)).fetchone()
        if not row:raise HTTPException(404,'Proposal not found')
        if row['status']!='Pending':return {'status':row['status'],'result':json.loads(row['result']) if row['result'] else None}
        authorize_role(user,row['action'])
        if data.decision=='reject':
            db.execute("UPDATE proposals SET status='Rejected' WHERE id=?",(pid,))
            return {'status':'Rejected'}
        payload=json.loads(row['payload'])
        if baseline(db,row['action'],row['target'],payload)!=row['baseline']:
            db.execute("UPDATE proposals SET status='Stale' WHERE id=?",(pid,))
            return {'status':'Stale','message':'Record changed since proposal. Ask for a new proposal.'}
        from .modules.students.application import create_student,edit_student,mark_attendance
        from .modules.notices.application import create_notice,edit_notice
        from .modules.support.application import create_task,resolve_task,add_note
        from .modules.finance.application import add_teacher,edit_teacher,record_expense,update_payroll_status,allocate_budget
        from .modules.rooms.application import commit_move
        from .modules.absences.application import record_absence
        from .modules.timetable.application import create_event
        handlers={
            'create_student':create_student,'edit_student':edit_student,'mark_attendance':mark_attendance,'record_absence':record_absence,'create_school_event':create_event,
            'create_notice':create_notice,'edit_notice':edit_notice,'create_task':create_task,
            'resolve_task':resolve_task,'add_note':add_note,
            'create_teacher':add_teacher,'edit_teacher':edit_teacher,'record_expense':record_expense,
            'update_payroll_status':update_payroll_status,'allocate_budget':allocate_budget,
            'move_session':commit_move
        }
        validated=validate(row['action'],payload)
        token=active_connection.set(db)
        try:
            args=[validated] if row['target'] is None else [row['target'],validated]
            result=jsonable_encoder(handlers[row['action']](*args))
        finally:active_connection.reset(token)
        db.execute("UPDATE proposals SET status='Accepted',result=? WHERE id=?",(json.dumps(result),pid))
        return {'status':'Accepted','result':result}

This page, measured

What this page is made of, measured from its own checkout: the history from git, the size and the branching from a plain reading of the source, and the same reading applied to the libraries it is built on. Nothing here is typed in; bun run stats writes it all before a build.

commits since June 2024
209
pull requests merged
36
lines of code written for this page
12,720
days with a commit, out of 848
26
paths through an average function
2.5
of the commits authored with Claude
49%

When

Commits a month, Jun 2024 to Oct 2026

  1. June 2024: 61
  2. July 2024: 6
  3. August 2024: 1
  4. September 2024: 0
  5. October 2024: 6
  6. November 2024: 0
  7. December 2024: 0
  8. January 2025: 0
  9. February 2025: 0
  10. March 2025: 0
  11. April 2025: 0
  12. May 2025: 0
  13. June 2025: 0
  14. July 2025: 0
  15. August 2025: 0
  16. September 2025: 0
  17. October 2025: 0
  18. November 2025: 0
  19. December 2025: 0
  20. January 2026: 10
  21. February 2026: 0
  22. March 2026: 0
  23. April 2026: 0
  24. May 2026: 3
  25. June 2026: 0
  26. July 2026: 0
  27. August 2026: 0
  28. September 2026: 0
  29. October 2026: 78
  1. Mon : 27
  2. Tue : 51
  3. Wed : 9
  4. Thu : 7
  5. Fri : 27
  6. Sat : 36
  7. Sun : 8

What

62 files, 12,720 lines of code, 10% comment

  • TypeScript 54%
  • Astro 42%
  • Other 4%

Plus 13,069 lines of Gradus vendored for the demos and 8,860 of content and data. The longest file is utils/network-scene.ts, 1,184 lines of it, and the most branching one utils/network-scene.ts, with 331 decisions.

Against the libraries it stands on

Paths through a function is McCabe's cyclomatic complexity, one plus the decisions (if, for, case, &&, ?:…) in it, averaged; the density is decisions per hundred lines. Each library is read as it is installed here: its source where it ships it, else its unminified build.

Code Lines Paths / function Decisions / 100 lines Lines / function Comment
This page src, scripts and tests, Gradus and data aside 12,720 2.45 17.8 8.1 10%
Gradus, the slice here the components the demos run 13,069 2.12 13.5 8.3 5%
three.js 0.186.1 the 3D library behind the sky 75,583 2.13 16.0 7.1 42%
RxJS 7.8.1 Angular's streams 8,214 1.43 10.6 4.1 60%
Lenis 1.1.6 the smooth scroll, as built 664 2.58 24.2 6.5 1%
Angular core 19.2.25 the framework under the demos, as built 24,162 2.17 22.9 5.1 41%
Astro 4.11.5 the site builder, as built 44,080 1.97 16.1 6.1 1%

The numbers themselves

stats.json, as of 2026-10-06 at 05bae09, opened in Gradus's own JsonViewer, the component its chat uses for a tool's answer.

Loading the numbers…