# Edu Wass > Senior Full-Stack Engineer building production systems and high-performance web platforms with TypeScript, PHP, and modern backend stacks. Source: https://eduwass.com/ --- # Contact Source: https://eduwass.com/contact/ You can reach me at: **eduwass [at] gmail.com** If you'd prefer to chat, book a slot below: --- # About Source: https://eduwass.com/about/ - [Resume](/resume/): Check my experience. Available to download or print. - [Testimonials](/love/): A collection of kind words from clients and colleagues. --- # Resume Source: https://eduwass.com/resume/ # Edu Wass Senior Software Engineer (Full-stack) ## About I'm Edu Wass, a senior full-stack engineer with a Computer Science background and 10+ years of experience building production web platforms and backend-heavy systems. I specialise in system design, API architecture, real-time features, and performance optimisation. I'm comfortable owning features end-to-end — from scoping with stakeholders through to shipping and rollout — and I make pragmatic decisions when trading off speed and completeness. Comfortable across TypeScript and PHP, from Node.js and custom APIs to database modelling and cloud infrastructure. I've worked across startup-to-scale transitions, managing technical debt while keeping systems reliable. Outside of work you'll find me enjoying music production, sports and fitness, or gaming. ## Work Experience ### 2020 — Now — Senior Full-stack Engineer / Consultant — Toptal & Arc.dev — Remote Senior full-stack consulting across product teams at growth-stage companies, owning features from scoping through to production. Focused on platform architecture, API design, performance, and reliable delivery. - [MailCharts.com](https://web.archive.org/web/20250127114105mp_/http://MailCharts.com): Owned development of the marketing and product platform for an email intelligence tool used by data-driven marketing teams. Built scalable content systems and maintained high-availability front-end infrastructure. - [Harver.com](https://web.archive.org/web/20250127114105mp_/http://Harver.com): Led redesign and implementation of a high-scale assessment platform serving enterprise customers, with a focus on reliability and performance under load. - [GirlsWhoCode.com](https://web.archive.org/web/20250127114105mp_/http://GirlsWhoCode.com): Improved platform performance and stability for a high-traffic nonprofit site running large-scale campaigns. - [MediaMath.com](https://web.archive.org/web/20250127114105mp_/http://MediaMath.com): Designed and built an internal product catalogue system, working across data modelling, API integration, and orchestration within a wider martech ecosystem. ### 2025 — Now — Lead Engineer — EventIntel — Remote Building an event intelligence platform integrating third-party APIs including Ticketmaster and RateHawk. Designed the system architecture, data models, and API layer from scratch. Shipping iteratively in a lean setup with a strong focus on getting working product in front of users fast. ### 2016 — 2018 — [CTO at Container Studio](https://web.archive.org/web/20180203092811/http://containerstud.io/) — London / Barcelona Technical lead at a small product studio, responsible for architecture decisions, stack choices, and delivery across client projects. - Built a headless architecture bridging a PHP CMS backend with a React front-end via a custom REST API layer, allowing teams to decouple content management from UI delivery. - Built containerised deployment infrastructure using Docker with PHP, Nginx, Git, and webhooks, cutting environment setup time significantly and standardising deployments across projects. - Designed and shipped UX and front-end for Harbour Space, a technology university in Barcelona, and led development of the new Ubiqum platform, an international coding bootcamp. ### 2017 — [Lecturer at HARBOUR.SPACE University](https://harbour.space/faculty/edu-wass) — Barcelona Lectured on technology stack decisions across MVP, growth, and scaling stages — covering infrastructure, backend, mobile, and front-end tradeoffs. Applied and practical, aimed at students building real products. ### 2016 — [Lead Developer at Tribe Interactive](https://www.madebytribe.com/) — Remote Led development across ecommerce client projects, with a focus on growth automation, conversion optimisation, and custom platform builds. Worked closely with the commercial team to understand client goals and translate them into technical solutions. ### 2015 — 2016 — [Full Stack Developer at TheGeorge](https://www.iamthegeorge.com/home/en) — Barcelona Full-stack engineering at an international agency working with large-brand clients including Microsoft, Pearson, Samsung, and Nestlé. - Designed and built the Microsoft Windows 10 Italy campaign site, a high-traffic promotional platform coordinating content from Italian YouTubers to drive product adoption. - Architected and delivered the Monza F1 circuit website, solving for spike traffic during race season using caching and load balancing techniques. - Built the Wall Street English multisite network (~25 country sub-sites), designing shared infrastructure with per-locale customisation. - Planned and built the backend of AskCucu, an Airbnb-style student housing platform for the Asian market. ## Education ### 2010 — 2016 — [BSc Computer Science at Universitat Politècnica de Catalunya](https://web.archive.org/web/20250127114105/https://www.upc.edu) — Barcelona Computer Science degree from one of Spain's leading technical universities, covering algorithms, systems design, databases, and software engineering fundamentals. ## Certifications ### 2018 — [Startup School 2018 — Y Combinator](https://web.archive.org/web/20250127114105/https://www.startupschool.org/) --- # One Theme, Every Tool: monotheme Source: https://eduwass.com/building-monotheme/ Date: 2026-06-24 I change color themes the way other people change desktop wallpapers. The problem: my "environment" isn't one app, it's twenty. Ghostty, tmux, Neovim, VSCode, Cursor, Zed, btop, lazygit, yazi, fzf, bat, opencode, Claude Code, even the macOS accent color and Raycast. Switching themes meant hand-porting the same six colors into twenty config formats, getting bored halfway, and living with a half-themed setup forever. So I built [monotheme](https://github.com/eduwass/monotheme): one source of truth, one command, everything reskins at once. ```bash theme set ``` Repo: **[github.com/eduwass/monotheme](https://github.com/eduwass/monotheme)**. ## The canonical format problem The naive approach is to invent your own theme schema — a little YAML file with `background`, `foreground`, eight ANSI colors — and generate everything from that. I tried. It falls apart the moment a real editor wants syntax highlighting, because "the color of a comment" isn't one color. It's the resolution of a TextMate scope like `comment.line.double-slash.ts` against ~140 scope rules with fallbacks. A 16-color palette can't represent that. So I flipped it: the **canonical format is the full VSCode theme** — the fat one, all 140-odd TextMate scopes intact. That's the source of truth. Everything else *projects down* from it. - A dumb tool (fzf, btop) gets the normalized 16-ANSI palette. - A smart tool (Neovim, bat) gets the full scope resolution. Keep the richest representation as canonical, lose nothing, throw away detail only at the edges where the target can't use it anyway. ## The pipeline Four stages, each boring on purpose: ``` load → project → adapter → target ``` `load` parses the VSCode theme JSON. `project` derives the normalized palette — bg, fg, ANSI 16, accents — and exposes `resolveToken`, which maps any TextMate scope to its color through a matcher algorithm I ported from VSCode's own resolver. Then each **adapter** renders that into one tool's native format, and each **target** writes the file to the right place and reloads the running app. The split between adapter (what to write) and target (where to put it and how to reload) is the part that made the whole thing extensible. Adding a new tool is one adapter + one target, and it never touches the core. ## Live reload `theme set` doesn't just write configs — it reloads the apps that are already running. tmux gets a `source-file`, Ghostty re-reads, Neovim instances get poked. The win isn't really the command, it's that there's no follow-up: no "now restart your terminal," no stale half-themed windows. You run one command and the whole screen flips. ## Install Needs [Bun](https://bun.sh). ```bash git clone https://github.com/eduwass/monotheme cd monotheme && bun install && bun link ``` That puts a `theme` command on your PATH: ```bash theme list # installed + bundled themes theme set # project to every tool + live-reload theme current # active theme theme check # self-check, no writes ``` Drop `theme init` in your shell rc so a fresh shell re-applies the active theme, and you're done. It won't touch tools you don't have — if Zed isn't installed, that target is skipped. Start with one or two tools, add adapters as you go. Repo: **[github.com/eduwass/monotheme](https://github.com/eduwass/monotheme)**. --- # A Raycast-Style Command Palette for tmux Source: https://eduwass.com/tmux-palette/ Date: 2026-05-13 Tweeted a screenshot of this earlier today, people asked for the code, open-sourced it the same day: **[github.com/eduwass/tmux-palette](https://github.com/eduwass/tmux-palette)**. tmux has a million commands and zero discoverability. You either know the bind or you don't. Raycast fixed that for macOS apps. This does the same for tmux: hit a key, fuzzy-find, run. ## The opentui detour First version used [opentui](https://github.com/anomalyco/opentui) — flexbox layout in the terminal, theming, mouse events. Tutorial-clean code, ~50 lines per palette. It shipped. It was slow. ``` === bun no-op === 0.00 s === bun + opentui import === 0.19 s ``` opentui ships native Yoga bindings. They deserialize on import. Every popup open paid 190ms loading them before drawing a single character. Felt like a 200ms hiccup every time. `bun build --compile` doesn't help — it bundles the runtime, not the imports. ## The rewrite I dropped the framework. The renderer now writes ANSI escape codes straight to stdout, like the prototype I started with. ```ts stdout.write("\x1b[?2026h\x1b[?25l\x1b[H" + frame + "\x1b[?2026l") ``` That's the whole render call. `?2026h/l` is synchronized output — tmux 3.4+ swaps the frame atomically, so holding the arrow key doesn't flicker. Cold start went from 190ms to 20ms. The framework gained ~100 lines for manual width/scroll/mouse handling. Each palette stayed the same size. Net wash on lines, 10× speedup. ## User-land config Customization lives in `~/.config/tmux-palette/`. Four JSON files, one job each. Add your own commands without forking: ```json // ~/.config/tmux-palette/commands.json [ { "icon": "", "title": "Toggle Diff Viewer", "category": "Tools", "action": { "tmux": "run-shell '~/scripts/diff-viewer.sh'" } } ] ``` `shortcuts.json` overrides the right-side label. `theme.json` overrides colors. `aliases.json` adds chips. The source-level `definePalette()` API is still there for power users, but nobody should have to fork to label a key. ## The dispatch trick tmux's `confirm-before` and `command-prompt` need stdin. If you run them *inside* the popup, they hang — the popup owns stdin. So the palette doesn't run the command. It writes the encoded command to a tempfile and exits. The bash wrapper reads the file *after* `display-popup` returns and runs it in the host context. Prompts get stdin, users press y/n, world keeps spinning. ```bash tmux display-popup -E "TMUX_PALETTE_CMD='$CMD_FILE' bun src/cli.ts" [ -s "$CMD_FILE" ] && eval "tmux $(cat "$CMD_FILE")" ``` Embarrassingly long to figure out the first time. Sub-100-byte fix. ## Install ```bash git clone https://github.com/eduwass/tmux-palette ~/Sites/tmux-palette cd ~/Sites/tmux-palette && bun install ``` Then in `.tmux.conf`: ```tmux bind -n C-Space run-shell "~/Sites/tmux-palette/bin/tmux-palette.sh" ``` Reload, hit Ctrl+Space. The README has an agent-handoff prompt — paste it into Claude Code / Codex / opencode / Cursor and it does the install, asks you which key to bind, optionally reads your terminal config and writes a matching theme. Onboarding via "paste this into your agent" feels right for 2026. Repo: **[github.com/eduwass/tmux-palette](https://github.com/eduwass/tmux-palette)**. --- # Replacing WordPress with 2,300 Lines of Bun Source: https://eduwass.com/replacing-wordpress-with-2300-lines-of-bun/ Date: 2026-04-23 The site you are reading is around 2,300 lines of TypeScript, Edge templates, and CSS in `src/`. It scored 99/90/100/100 on PageSpeed the first time I ran it, with no performance work done. It builds in under half a second and deploys to a Cloudflare Worker in under 30 seconds on warm caches. It used to be WordPress. This is the story of why and how I replaced a perfectly working WordPress site with a hand-rolled static site generator, what I built instead, and the weird decisions that ended up being the right ones. ## The old setup The old eduwass.com was a WordPress install running locally on LocalWP, statically exported to a `/static/` directory via Simply Static, pushed to GitHub, and deployed as a Cloudflare Pages project watching `static/*`. The theme was a custom thing pieced together with the usual plugin pile: Perfmatters for lazy loading, ASE for email cloaking, a dark-mode toggle block, Yoast for SEO, the works. It worked. It was also constantly costing me small bits of attention: plugin updates, theme drift, a PHP-ish local dev experience, an export step that occasionally produced weirdness, a local MySQL service running for no reason when I'd rather just edit markdown. The irony was that I was writing plain content in the WordPress editor, then the export produced HTML files, then CF served them statically. At no point in the served-to-visitor path was there any dynamic WordPress behavior. I was paying WordPress's full complexity budget for the authoring side and getting a static HTML stack anyway. So I ripped it up. ## The goal Hard constraints I wrote down before starting: - Every line in the repo is something I understand and can change. No "framework did this magic." - Content stays portable. Markdown with YAML frontmatter, so if I ever want to move to Astro or Hugo, I copy `content/` and plug it in. - Dev loop has to feel instant. Not "hit save, see reload a second later" instant — *actually* instant, with state preservation. - No "click a link, wait for a blank page, see content" navigation feel. Prefetching, speculation rules, whatever it takes. - Feature parity with the old site's quality-of-life stuff: email cloaking, redirect shortlinks (including cloaked ones that hide the destination), analytics, a GitHub contribution graph, syntax-highlighted code blocks. I didn't write "deploy to Cloudflare" explicitly but it was implicit — the old site already lived there and I liked the edge-first distribution. ## The stack Five production dependencies: ``` edge.js templating shiki syntax highlighting edge-iconify icon rendering @iconify-json/* icon data (Lucide + Simple Icons) ``` Bun handles everything else: it's the runtime, the bundler, the dev server, the YAML parser, the markdown parser, the TypeScript transpiler. No Vite, no Webpack, no Astro runtime, no Node.js installed. One tool. This was a deliberate bet. Bun is new enough to be nervous about, but it's good enough at each of those individual jobs that pulling in a separate tool for each felt like adding weight for marginal quality. ## The build pipeline `src/build.ts` is 103 lines. Top to bottom, it: 1. Wipes `dist/` and rebuilds CSS and JS with content-hashed filenames. 2. Copies static media (avatar, favicons). 3. Runs an idempotent optimization pass: any raw PNG/JPG becomes AVIF at q60; any MP4 over 1.5MB gets re-encoded with x264 CRF 28 and audio stripped. On an already-optimized repo this is a no-op. 4. Loads every content collection in parallel (posts, pages, projects, site config, redirects, home content). 5. Fetches the GitHub contribution graph via `gh api graphql` at build time (no token shipped; the local `gh` CLI auth is trusted). 6. Constructs the speculation-rules URL list (every internal page, so the browser can prerender on hover). 7. Creates one Edge instance per build with all those collections attached as globals. 8. Renders each page, copies colocated assets, writes redirects, writes the sitemap, RSS feed, and robots.txt. On a real run: 28 pages, 17 redirects, ~300ms. Warm dev server rebuilds in ~100ms. ## Content as portable markdown Content lives in `content/`. Every page is markdown with YAML frontmatter: ```markdown --- title: About layout: about links: - title: Resume href: /resume/ description: Check my experience. - title: Testimonials href: /love/ description: Kind words from clients and colleagues. --- ``` The `layout:` field names a template in `src/templates/pages/`. If absent, it falls back to the default prose wrapper. This is the same convention Hugo and Jekyll use, which means `content/` is fully portable: rename a file, drop it into an Astro or Hugo project, and it works. The resume is the interesting case. It's a `.md` file with *no* body — everything is structured frontmatter (name, role, experience array, education array, etc.) — and it points at `layout: resume`, which is a standalone Edge template that emits its own `` (no site chrome, matches how resumes want to look). I experimented briefly with `.edge` files holding both data and layout in one file — a sort of single-file page format — and it's clever but it makes content less portable. Reverting to "content is markdown, layouts are templates" was the right call. ## The HMR trick The dev server is where I spent the most design time, because I wanted it to feel *better* than framework dev servers, not worse. The standard full-reload approach wastes everything. You're iterating on a CSS tweak, you scroll to the bottom of a long post to check a margin, save, and the page reloads back to the top. Every time. Intolerable. So the HMR client (`src/dev/hmr-client.ts`, 90 lines) does a morph instead. On every file save: 1. Fetch the current URL's new HTML. 2. If the hashed CSS link changed, swap the `` — browser re-fetches styles, no flash. 3. If the hashed JS bundle changed, inject a fresh `