<SYSTEM>This document contains comprehensive information about Leonardo Trapani's professional profile, portfolio, and blog content. It includes personal details, work experience, projects, achievements, certifications, and all published blog posts. This data is formatted for consumption by Large Language Models (LLMs) to provide accurate and up-to-date information about Leonardo Trapani's background, skills, and expertise as a Design Engineer.</SYSTEM>

# leotrapani.com

> A minimal, pixel-perfect dev portfolio, component registry, and blog to showcase my work as a Design Engineer.

## About

20 y/o technical founder and startup engineer based in Milan.

At [Datapizza](https://datapizza.tech) (#8), I'm leading Dual, building an agent platform that brings AI into enterprises.

Outside of work I ship and contribute to open-source projects, train MMA, and live my university life in Milan.

### Personal Information

- First Name: Leonardo
- Last Name: Trapani
- Display Name: Leonardo Trapani
- Location: Milan, Italy
- Website: https://leotrapani.com

### Social Links

- [GitHub](https://github.com/leonardotrapani)
- [LinkedIn](https://linkedin.com/in/leonardotrapani)
- [X](https://x.com/leo_trapani)
- [YouTube](https://www.youtube.com/@leotrapani)

### Tech Stack

- [TypeScript](https://www.typescriptlang.org/)
- [Python](https://www.python.org/)
- [Go](https://go.dev/)
- [Bun](https://bun.sh/)
- [React](https://react.dev/)
- [TanStack](https://tanstack.com/)
- [Next.js](https://nextjs.org/)
- [Tailwind CSS](https://tailwindcss.com/)
- [shadcn/ui](https://ui.shadcn.com/)
- [Radix UI](https://www.radix-ui.com/)
- [Git](https://git-scm.com/)
- [Docker](https://www.docker.com/)
- [Datapizza AI](https://datapizza.tech/en/ai-framework/)
- [Linux](https://www.linux.org/)
- [Vite](https://vite.dev/)
- [PostgreSQL](https://postgresql.org/)
- [AWS](https://aws.amazon.com/)
- [MongoDB](https://www.mongodb.com/)
- [Redis](https://redis.io/)
- [Bash](https://www.gnu.org/software/bash/)

## Experience

### Founding Engineer / Technical Lead | Datapizza

Duration: 06.2023 - Present

Skills: Effect.ts, TypeScript

- Joined as employee #8 and helped Datapizza grow to 90+ people
- Leading Dual, building an agent platform on top of enterprise systems
- Led Datapizza Jobs, building products for candidates, companies, and internal recruiters handling 100,000+ candidate pipelines

### Software Engineer | Freelance

Duration: 09.2022 - 09.2025

Skills: N/A

Built commissioned projects and helped start small startups.

- Nutrivetpet: built a pet nutrition platform from zero to production, serving 3,000+ monthly paying users
- Sintesy: built a mobile app with real-time connections to oxygen sensors
- More: websites, internal tools, dashboards, and product MVPs

### Bocconi University | Education

Duration: 09.2025 - 2028 (Expected)

Skills: N/A

Bachelor's degree in Economics, Management, and Computer Science

## Projects

### Hyprvoice

Project URL: leonardotrapani/hyprvoice

Skills: Go, Linux

open-source voice typing for Wayland/Hyprland.

- press a key, speak, and inject text anywhere
- supports 26 cloud and local transcription models, including whisper.cpp
- ships with AUR packaging, onboarding, hot-reload config, and streaming support

### XSchema

Project URL: https://xschema.dev

Skills: Go, TypeScript

JSON Schema toolchain for generating native validators across languages.

- one pipeline for parsing, fetching, validating, resolving refs, and generating code
- adapters for Zod, ArkType, Effect Schema, Valibot, Pydantic, and more
- compliance reports against the official JSON Schema Test Suite

### Rediredge

Project URL: https://rediredge.app

Skills: Go, TypeScript

self-hostable redirect infrastructure for domains.

- manage domains and redirect rules from a dashboard
- serve redirects from a stateless Go data plane backed by Redis
- run hosted or self-host with Docker Compose

## Awards



## Certifications



## Blog

---
title: "OpenClaw: just treat it like a real assistant"
description: "Perimeter security is the easy part. A practical framework to run OpenClaw with real agency without handing it your identity."
last_updated: "February 7, 2026"
source: "https://leotrapani.com/blog/openclaw-just-treat-it-like-a-real-assistant"
---

# OpenClaw: just treat it like a real assistant

Perimeter security is the easy part. A practical framework to run OpenClaw with real agency without handing it your identity.

## The secure OpenClaw setup

It is possible to make a secure OpenClaw setup, even if you give it credit card access.

But most “secure OpenClaw setups” are not secure.

I keep seeing posts like:

> “I deployed OpenClaw on a VPS, put it behind Cloudflare Tunnel, no ports open, we’re safe.”

Cool. That solves one problem. (Reference: Cloudflare’s write-up on the “VPS + Tunnel” approach: [https://blog.cloudflare.com/moltworker-self-hosted-ai-agent/](https://blog.cloudflare.com/moltworker-self-hosted-ai-agent/))

Then the same people give the agent access to their primary inbox, their main Google account, random API keys, and whatever else it asks for. At that point, the tunnel is security theater.

Now the real question is: would you do that with a real assistant?

Would you give them your personal inbox with full send access? Your main Google account? Your primary credit card? Your entire computer?

Of course not.

That is the whole philosophy here: treat OpenClaw like a real assistant. Useful, trusted, but not you.

A real assistant could go crazy one day and start sending emails to everyone saying you’re a terrible person.

That would be catastrophic, but it would not be *you*. It would be your assistant.

Most AI setups accidentally collapse that boundary by giving the agent your main identity, your main inbox, your main money.

Even worse, this is not theoretical. In the past few days multiple reports came out about **malicious OpenClaw skills on ClawHub**, including cases where the **most-downloaded** add-on was described as a malware delivery vehicle.

That’s the point: once you install skills, you’re not only securing a VPS. You’re managing a supply chain that can execute with real permissions.

So the question isn’t “is my server reachable?”. The question is: if one input, one skill, or one token goes bad, can the agent still do useful work without being able to impersonate you?

## A realistic threat model (because yes, it happens)

An AI assistant is software that:

* consumes untrusted input (email, web pages, documents, chats)
* makes decisions under ambiguity
* calls tools using credentials

That’s a nasty combination of:

1. **uncertainty** (LLMs can misunderstand)
2. **power** (tools can do real damage)

A key risk people point out is **excessive agency**, but I think that framing is slightly misleading.

Agency is the whole point. You *want* the agent to be able to do things. Browse the web. Touch the filesystem. Even have credit card access.

The real issue is what that agency can do **to your real identity and your real assets** once something gets penetrated.

If the agent gets prompt-injected or compromised, your goal is that your online life is not over. It should not be able to email people from your main account, drain your main card, or pull the keys to your entire digital life off your primary machine.

The fix is not “trust the model more”. The fix is: **least privilege + separation + human-in-the-loop**.

So you need a setup that looks more like “a company with separation of duties”, even if it’s just you.

## My setup: separate everything (for real)

### 1) Separate email and calendar (Workspace account)

I keep two distinct worlds:

* my personal account: my life
* the AI account (Google Workspace): the assistant

The assistant lives in the Workspace account.

When I want it to see my personal email, I **forward** specific emails to the AI inbox (or I just set up auto-forwarding). I **share** my calendar and it creates events and invite me.

Rule of thumb: the assistant should **never** have write access to my main accounts. No “just this once”, no “for convenience.”

Why this matters:

* prompt injection in a random email can’t make it reply from your main identity
* if the AI account gets compromised, revoking access is a clean cut
* you can keep permissions explicit: what it can read, what it can write, and where

### 2) Password manager: a dedicated vault + service account access

Secrets should not live in prompts. Full stop.

I use 1Password with:

* a **dedicated vault** just for the assistant
* access via a **service account token**, not human credentials

This gives you two huge wins:

* clear scope: the agent can only see what’s inside that vault
* easy revocation: invalidate the token, and the blast radius collapses

More generally: the assistant shouldn’t “know passwords”. It should **fetch secrets just-in-time**, narrowly scoped to the tool it’s using.

Set up openai whisper? Say "Hey I added the Openai api key to your 1Password vault, please set up whisper"

### 3) Yes, you can give an agent a credit card

You’d think you’d never find “give it a credit card” in a security article.

But if you follow this framework, you can.

Give the agent a **virtual card** with a hard budget limit. Put the card details in the agent’s **dedicated 1Password vault**. Now the agent can pay for things when it needs to, but the worst-case damage is capped.

### 5) Separate GitHub identity (and every other important accounts)

If the agent writes code (OpenCode, Claude Code, whatever you use), it should do it from its own GitHub account.

Give it an email (the Google workspace one in this example), create a dedicated GitHub identity, and have it open PRs from that account. It should never have access to your main GitHub account.

This gives you a clean boundary: if it goes off the rails, it’s a compromised contributor, not a compromised you. You could push directly to main and delete everything, your assistant couldn't.

### 6) Human-in-the-loop for irreversible actions

Anything irreversible (or high-impact) should require either:

* explicit confirmation, or
* a workflow that goes through a staging area (draft) and only gets executed after review

Email is the obvious example:

* the agent can draft
* you decide whether to send

This will prevent most disasters, but it’s not a silver bullet.

We can never be sure prompt injection won’t slip through email or web content, even if the agent is instructed to “always go through you.”

That’s why you do all the steps above. The goal is that even when something goes wrong, the agent still can’t impersonate you, drain your money, or walk away with the keys to your life.

This is the zero-trust idea applied to agents: **the LLM should not be the final authority on permissions.**

## Closing

None of this is bulletproof.

Prompt injection will always be a problem. Supply chain attacks will keep happening. Models will misunderstand. Tools will fail.

The goal is not perfection. The goal is survivability.

If things go wrong, you want a setup where:

* the agent cannot send email from your main identity
* the agent cannot drain your main card
* the agent cannot quietly exfiltrate the keys to your entire digital life

That’s what separation, least privilege, and human-in-the-loop give you. Treat it like a real assistant: trusted, sure, but still new.

Give it tools to work, not the keys to your house.


Last updated on February 7, 2026

---
title: "Datapizza Salaries Manifesto"
description: "Salaries is our platform for mapping the Italian tech market through detailed statistics on salaries, roles, and trends. This is our manifesto: the principles that guide us and how we put them into practice."
last_updated: "December 9, 2025"
source: "https://leotrapani.com/blog/salaries"
---

# Datapizza Salaries Manifesto

Salaries is our platform for mapping the Italian tech market through detailed statistics on salaries, roles, and trends. This is our manifesto: the principles that guide us and how we put them into practice.

> Data from the community, for the community.

Read this on [Datapizza's Blog](https://datapizza.tech/it/blog/ifgc/)

## **For a competitive Italy in tech**

For years, Datapizza has been working to make Italy competitive in tech. With Datapizza Jobs, we introduced "transparent compensation" in job postings. With our Open Source framework [Datapizza AI](https://github.com/datapizza-labs/datapizza-ai), we support the development of technologies built by the Italian community. Through our content and events, we strive to grow an ecosystem that can compete internationally.

[Datapizza Salaries](https://salaries.datapizza.tech) is born from the same vision: salary transparency is not a luxury, but a necessity for a healthy tech market.

## **Our principles**

Building a platform "from the community, for the community" is not a slogan. It's a design constraint that influences every technical decision. Here's what it means to us:

**Real anonymity.** Personal data is never linked to submissions. We don't ask for accounts, we don't track identities, we don't request sensitive information. The anti-abuse mechanisms we use (more on this later) are designed to be completely separated from the data we collect.

**Verifiable transparency.** It's not enough to say "we're transparent." The complete dataset is automatically published every week on Hugging Face. Anyone can download it, analyze it, verify that the numbers on the platform match the raw data.

**Giving back value.** The community gives us the data, we return insights. The platform is built to not require authentication, to not force anyone through our channels: the data must be as accessible and transparent as possible.

**Respect for contributors.** The submission form is essential. We only ask for what's needed to produce useful statistics, without authentication or unnecessary steps. If someone from the community dedicates two minutes to contribute, those two minutes must count.

**Data integrity.** Without authentication, we must protect the dataset from spam and manipulation. We do this with technologies that don't compromise anonymity, finding the balance between openness and reliability.

## **How we do it**

Let's move from theory to practice. This section is more technical: if you're only interested in using the platform, you can skip directly to the [conclusions](https://www.notion.so/Manifesto-di-Datapizza-Salaries-2bf04d204fa2809e83cced9d458bd84e?pvs=21). If you want to understand how we've implemented these principles, keep reading.

### **Public dataset on Hugging Face**

Every week, a cronjob automatically publishes the entire dataset on Hugging Face through their APIs. Not a selection, not a sample: all the data, with a flag indicating whether each submission has passed integrity checks.

The dataset is released under **CC-BY-NC-4.0** license, so it remains completely open source and the data remains property of the community, and won't be used for commercial purposes.

[Go to Dataset](https://huggingface.co/datasets/datapizza-ai-lab/salaries)

This means you can:

* Verify that the statistics shown in the app match the raw data
* Do your own analyses with the criteria you prefer
* Independently decide whether to include or exclude submissions based on the validation flag

Automatic publication also eliminates any doubt that the data is being "curated" before release.

### **Transparency in the app**

When you browse [salaries.datapizza.tech](https://salaries.datapizza.tech/), every statistic shows the number of submissions it's calculated from. Not just the median value, but also how many data points support it.

This serves two purposes:

1. It allows you to assess the reliability of each number. A median calculated from 500 submissions is more robust than one calculated from 12.
2. It makes it evident where more data is needed, incentivizing targeted contributions.

### **Anti-bot protection: Cloudflare Turnstile**

The submission form is protected by [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/), an alternative to traditional CAPTCHAs that verifies users without frustrating puzzles. It's the first line of defense against bots and automated spam.

### **Fingerprinting for integrity: Thumbmark.js**

Without authentication, how do we prevent someone from submitting dozens of submissions to alter the statistics?

We use [Thumbmark.js](https://github.com/nicktrienenern/thumbmark), a library that generates a browser fingerprint based on technical characteristics (canvas rendering, available fonts, WebGL capabilities, etc.). This fingerprint is a hash that identifies a browser/device combination, not a person.

**The crucial point: the fingerprint is never linked to the submission.** It's saved in a separate MongoDB collection, with a dynamic TTL (time-to-live). If no submissions arrive with that fingerprint for x weeks, it's automatically deleted.

The flow is:

1. Upon submission, we generate the fingerprint
2. We check if it already exists in the collection
3. If it exists and has recent submissions, we flag the new submission as "to be verified"
4. The fingerprint itself is updated (TTL reset) but remains separated from the data

This system doesn't aim to prevent every individual in the world from sending data twice (someone could use different devices on different networks), but thanks to the volume of data we manage, it's a reasonable compromise between protection and privacy, and the best solution to avoid linking an email. In the public dataset, each submission has a flag indicating whether it passed this check: you decide whether to trust it or not.

## **The architecture**

The infrastructure is designed to support the principles we've described: easy access to data, uncompromised performance, and a maintainable codebase.

### **Next.js with aggressive caching**

The app is built in Next.js, with almost everything rendered server-side. We use Next's native caching for pages:

```javascript
// Each job title page is regenerated at most once a week
export const revalidate = 3600 * 24 * 7;
```

For each job title, we pre-generate all pages at build time with `generateStaticParams`:

```javascript
export async function generateStaticParams() {
  const jobTitles = await getJobTitles();
  return jobTitles.map((jobTitle) => ({
    "job-title-id": jobTitle.id.toString(),
  }));
}
```

This means each job title has its own pre-generated page, with data already cached, and with metadata (for SEO for example) already ready. When a user searches "Software Engineer salary Italy," the page is already there, cached and fast.

### **Python for aggregations: Polars**

Statistical aggregations require specialized libraries. Instead of reinventing the wheel in TypeScript, we use Python with [Polars](https://pola.rs/): a modern data analysis library, written in Rust, significantly faster than pandas.

Even with caching, aggregations must be performant for when the cache is invalidated or for new job titles. Polars allows us to calculate medians, percentiles, standard deviations, and multidimensional breakdowns in milliseconds.

### **Single source of truth: from Zod to Pydantic**

Here's a classic problem: we use TypeScript for Next.js and Python for aggregation APIs. How do we keep types, validations, and constants aligned?

Our solution: **TypeScript is the source of truth**. We define everything in [Zod](https://zod.dev/) (validation schema for TypeScript), then automatically generate:

1. JSON Schema from the Zod schema
2. Pydantic models from the JSON Schema

Here's a simplified example to illustrate the idea. This Zod schema:

```typescript
export const aggregateResponseSchema = z.object({
  jobTitleId: z.number().int().positive(),
  count: z.number().nonnegative(),
  cards: z.array(
    z.object({
      seniority: z.enum(["junior", "mid", "senior"]),
      medianSalary: z.number().min(0).max(500000),
      count: z.number().nonnegative(),
      // ... other statistical fields
    })
  ).length(3),
  // ... other breakdowns
});
```

Is automatically transformed into JSON Schema, and then into Pydantic:

```python
class Card(BaseModel):
    model_config = ConfigDict(extra='forbid')
    seniority: Seniority
    medianSalary: confloat(ge=0.0, le=500000.0)
    count: confloat(ge=0.0)
    meanSalary: confloat(ge=0.0, le=500000.0)
    # ...

class AggregateResponse(BaseModel):
    model_config = ConfigDict(extra='forbid')
    jobTitleId: PositiveInt
    count: confloat(ge=0.0)
    cards: List[Card] = Field(..., max_length=3, min_length=3)
    # ...
```

The generation script uses `zod-to-json-schema` and `datamodel-codegen`

Here's a small snippet of this script:

```javascript
// Generate JSON Schema from Zod
const schema = zodToJsonSchema(aggregateResponseSchema, {
  name: "AggregateResponse",
});
writeFileSync("api/generated/aggregate-response-schema.json",
  JSON.stringify(schema, null, 2));

// Then, via shell, generate Pydantic
execSync(`datamodel-codegen \
  --input generated/aggregate-response-schema.json \
  --output generated/aggregate_response.py \
  --input-file-type jsonschema \
  --output-model-type pydantic_v2.BaseModel`);
```

This pattern also extends to constants: the list of Italian regions, seniority levels, industries—everything defined once in TypeScript, exported to JSON, imported in Python. Zero drift, zero alignment bugs.

### **FastAPI on Vercel Python Runtime**

The Python backend runs as a serverless function on [Vercel Python Runtime](https://vercel.com/docs/functions/runtimes/python). FastAPI handles aggregation requests, validates input and output with the generated Pydantic models, and communicates with Next.js through internal APIs (protected by a shared API key in the same environment).

This architecture allows us to have everything in a single Vercel deployment, without managing separate servers or external caching layers like Redis. Next.js caching is sufficient for our access pattern.

## **Conclusions**

Datapizza Salaries is our attempt to build something that was missing: a source of data on Italian tech work that is transparent, verifiable, and owned by the community that feeds it.

If you work in tech in Italy, [contribute with your submission](https://salaries.datapizza.tech/form). If you want to get your hands dirty, download the dataset and let us know what you find. If you have feedback, we're all ears.

And if this technical deep-dive has intrigued you and you want to know more about the architecture, how we manage multi-level caching, Polars aggregation patterns, type safety across multiple programming languages, developer experience, or Vercel deployment, reach out. We might make another blog post about it.

[**Explore Datapizza Salaries →**](https://salaries.datapizza.tech/)


Last updated on December 9, 2025

---
title: "Rediredge: Blazing-Fast Domain Redirects Without the DevOps Tax"
description: "Rediredge is a lightweight, self-hostable domain redirector that pairs a Go data plane with a Next.js control plane. It gives you instant 30x responses without cold starts, automatic HTTPS, and a beautiful dashboard that lets non-technical teammates manage redirects on their own."
last_updated: "December 26, 2025"
source: "https://leotrapani.com/blog/rediredge"
---

# Rediredge: Blazing-Fast Domain Redirects Without the DevOps Tax

Rediredge is a lightweight, self-hostable domain redirector that pairs a Go data plane with a Next.js control plane. It gives you instant 30x responses without cold starts, automatic HTTPS, and a beautiful dashboard that lets non-technical teammates manage redirects on their own.

Domain redirects should be invisible infrastructure.

If you want to redirect `cal.acme.com` to your Calendly, you usually have to choose between two equally frustrating paths:

1. **The CloudFront Path:** A forty-click journey through AWS consoles, CNAME records, certificate managers, and YAML incantations that break when you sneeze.
2. **The "Ask a Developer" Path:** A Slack message to engineering, a ticket in the backlog, and three days of waiting for someone to update a config file.

I was tired of the friction. I wanted something that felt invisible: a system where marketing could spin up `promo.example.com` without pinging the engineering team, where HTTPS just worked, and where the underlying infrastructure could handle millions of requests without blinking. So I built **Rediredge**.

## Not Another Link Shortener

You might be thinking: "Why not just use Bitly?"

Link shorteners solve a different problem. They give you a generic `bit.ly/xyz123` that lives on someone else's domain. That's fine for a tweet, but terrible for your brand.

When you use a link shortener for business-critical redirects, you're handing your SEO juice to a third party. Search engines see `bit.ly`, not `yourcompany.com`. You lose domain authority, you lose brand recognition, and you're at the mercy of another company's uptime.

Rediredge flips the script. Your redirects live on *your* domains. `go.acme.com/demo` points to your demo booking page, and search engines attribute the link equity to you. It's personalized, it's branded, and you own the infrastructure.

## How It Actually Works

The best infrastructure is the infrastructure you forget exists.

When someone on your team needs a new redirect, they shouldn't need to understand DNS propagation or TLS certificates. With Rediredge, you open the dashboard, type the source domain, paste the destination, and hit save. That's it.

Behind the scenes, the system handles TXT-based domain verification, provisions TLS certificates via ACME, and pushes the new rule to a Redis read model that the Go redirector queries in sub-millisecond time. But to the user? It's just a form.

That's the core philosophy: **Redirects are a product concern, not a DevOps ticket.**

## Self-Host or Let Us Handle It

Rediredge is designed for flexibility.

**Option 1: Hosted (zero setup)**
Use our horizontally-scaled service. We operate the Go redirectors, manage TLS/ACME, and handle the infrastructure. You bring your domains; we handle the rest.

**Option 2: Self-Host (one command)**
Run the data plane on your own infrastructure while we manage the control plane. Clone the repo, run `docker compose up -d`, and you're live.

```yaml
# Your server (self-hosted)
services:
  redirector:
    image: ghcr.io/leonardotrapani/rediredge-redirector
    ports:
      - "80:5498"
      - "443:5499"
    environment:
      - REDIS_URL=redis://redis:6379
  redis:
    image: redis:alpine
```

The Go redirector handles HTTPS via autocert (ACME HTTP-01), so you don't need a reverse proxy in front. Point your DNS, and certificates provision automatically.

## Why Go + Redis?

Most redirect solutions live on the critical path of your CDN or require expensive managed services. Rediredge takes a different approach.

The architecture splits cleanly into two planes:

* **Control Plane (Next.js):** A dashboard for auth, domains, and redirect rules. It persists canonical configuration to Postgres and publishes a read-optimized view to Redis.
* **Data Plane (Go):** A stateless redirector that handles TLS via autocert, reads from Redis, and returns instant 30x responses. No cold starts. No Lambda spin-up. Just raw speed.

```
Client → Go Redirector (TLS + HGET) → 30x Response
                ↓
             Redis (read model)
                ↑
         Sync Worker ← Postgres ← Dashboard
```

Because the Go redirector is stateless, you can scale it horizontally by just spinning up more instances behind a load balancer. Redis handles the coordination. The dashboard is never on the hot path.

## Under the Hood: The Outbox Pattern

The secret sauce that keeps everything in sync? The **Outbox Pattern**.

Every time you create or update a redirect in the dashboard, the change doesn't go directly to Redis. Instead, it lands in an outbox table in Postgres. A sync worker picks up these events and applies them to Redis idempotently. Each rule carries a `version` field, and the worker only applies updates if the incoming version is newer.

This gives you:

* **Durability:** If Redis goes down, events stay in the outbox and retry with backoff.
* **Consistency:** No split-brain scenarios where the dashboard and redirector disagree.
* **Rebuild capability:** A job can truncate and repopulate the Redis namespace from Postgres at any time.

Here's what a redirect rule looks like in the Redis read model:

```json
{
  "redirect:example.com:cal": {
    "to": "https://calendly.com/acme",
    "status": 308,
    "preservePath": false,
    "preserveQuery": true,
    "enabled": true,
    "version": 3
  }
}
```

The lookup is a single `HGET`. No parsing. No joins. Just instant responses.

## Installation and Contributing

Rediredge is open-source and currently in pre-alpha. If you want to try it out:

```bash
# Clone the repo
git clone https://github.com/LeonardoTrapani/rediredge.git
cd rediredge

# Install dependencies
bun install

# Start dev environment
bun run dev
```

Contributions are welcome. If you want to help make domain redirects a first-class experience, check out the repository.

[View on GitHub](https://github.com/leonardotrapani/rediredge?utm_source=leotrapani.com\&utm_medium=referral\&utm_campaign=portfolio)


Last updated on December 26, 2025

---
title: "Shimmering Text"
description: "Smooth shimmering text animation built with Motion and shared through my custom shadcn registry."
last_updated: "November 2, 2025"
source: "https://leotrapani.com/blog/shimmering-text"
---

# Shimmering Text

Smooth shimmering text animation built with Motion and shared through my custom shadcn registry.

```tsx
import { ShimmeringText } from "@/components/shimmering-text";

export function ShimmeringTextDemo() {
  return (
    <ShimmeringText className="text-2xl font-medium" text="Shimmering Text" />
  );
}

```

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @leotrapani/shimmering-text
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install motion clsx tailwind-merge
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx";
      import { clsx } from "clsx";
      import { twMerge } from "tailwind-merge";

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs));
      };

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/leotrapani/shimmering-text.tsx" showLineNumbers
      "use client";

      import type { HTMLMotionProps, Variants } from "motion/react";
      import { motion } from "motion/react";
      import * as React from "react";

      import { cn } from "@/lib/utils";

      export type ShimmeringTextProps = Omit<HTMLMotionProps<"span">, "children"> & {
        text: string;
        duration?: number;
        isStopped?: boolean;
      };

      export function ShimmeringText({
        text,
        duration = 1,
        isStopped = false,
        className,
        ...props
      }: ShimmeringTextProps) {
        const createCharVariants = React.useCallback(
          (charIndex: number): Variants => ({
            running: {
              color: ["var(--color)", "var(--shimmering-color)", "var(--color)"],
              transition: {
                duration,
                repeat: Infinity,
                repeatType: "loop" as const,
                repeatDelay: text.length * 0.05,
                delay: (charIndex * duration) / text.length,
                ease: "easeInOut",
              },
            },
            stopped: {
              color: "var(--color)",
              transition: {
                duration: duration * 0.5,
                ease: "easeOut",
              },
            },
          }),
          [duration, text.length]
        );

        return (
          <motion.span
            className={cn(
              "inline-block select-none",
              "[--color:var(--color-zinc-400)] [--shimmering-color:var(--color-zinc-950)]",
              "dark:[--color:var(--color-zinc-600)] dark:[--shimmering-color:var(--color-zinc-50)]",
              className
            )}
            {...props}
          >
            {text?.split("")?.map((char, i) => (
              <motion.span
                key={i}
                className="inline-block whitespace-pre"
                initial="stopped"
                animate={isStopped ? "stopped" : "running"}
                variants={createCharVariants(i)}
              >
                {char}
              </motion.span>
            ))}
          </motion.span>
        );
      }

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import { ShimmeringText } from "@/components/leotrapani/shimmering-text";
```

```tsx
<ShimmeringText text="slide to unlock" />
```


Last updated on November 2, 2025

---
title: "DX: I'm obsessed with developer experience (and you should be too)"
description: "DX is not fluff: it's the compounding force behind happier teams and faster products. Stories, tools, and a DX‑first ethos."
last_updated: "October 15, 2025"
source: "https://leotrapani.com/blog/dx"
---

# DX: I'm obsessed with developer experience (and you should be too)

DX is not fluff: it's the compounding force behind happier teams and faster products. Stories, tools, and a DX‑first ethos.

> DX isn't cosmetics. DX compounds.

I've lived both sides: weeks where shipping felt like flying, and weeks where every deploy felt like quicksand. The difference wasn't talent, willpower, or even time. It was **developer experience**.

DX is the set of tools, defaults, docs, and feedback loops that make builders fast **and** happy. When you tune DX, you don't just make code "nicer." You shorten the loop from idea → change → proof. That loop speed is the force‑multiplier.

***

## What I mean by DX

DX isn't one feature (like deployments). It's the experience surface that makes every change cheaper and safer. Here are some examples:

* **Tight feedback across the whole loop:** editor → type‑checker → tests → preview → prod. Fast linting, watch‑mode tests, instant type errors, and quick ways to prove changes.
* **Type safety and clarity by default:** strong typing, schemas/contracts between services, and APIs that compose—move fast *with* confidence.
* **Documentation & support as first‑class:** clear "golden path," examples, copy‑paste snippets, troubleshooting, and humans/communities that actually answer.
* **Single source of style:** one canon for how code and UI look—formatter, linter, naming conventions, design tokens, and a shared component library—so everything reads and feels consistent.
* **Coherent repos and tooling:** one place to find things, consistent scripts, shared packages; minimal context switching.
* **Guardrails & observability:** linters, formatters, CI checks, templates, logs/metrics that help you debug without spelunking.

***

## Misconceptions I hear (and what reality looks like)

* **"DX just makes things easier."** Yes—and that's the point. Removing friction raises velocity *and* satisfaction, which quietly improves quality and retention.
* **"DX = abstraction = less control."** Not necessarily. Abstraction can help (Vercel is, in a sense, a "wrapper on AWS"), but *good* DX is leverage, not hiding sharp edges at all costs. Some abstractions go too far for my needs—e.g., I don't reach for LangChain because it trades away composability and clarity in ways that slow me down. Abstraction can be the opposite of good DX when it removes the handles builders need.

***

## The industry is moving toward DX

Vercel reset expectations (previews, zero‑config deploys), but it's part of a broader shift. Tools like **Supabase** (sane auth/storage/DB defaults), **BetterAuth** (clean auth flows for modern stacks), **Drizzle** (type‑safe ORM), and **shadcn/ui** and its registries (copy‑pasteable UI primitives) all push the same way: shorten the path from idea to proof.

***

## Places I pushed DX until it changed outcomes

### [Datapizza](https://datapizza.com) — from 6 confused repos to one fast monorepo

**Before:** six repos, different pipelines, duplicate code, no internal registry; devs dreaded cross‑repo work.

**After:** a **monorepo** (Turborepo, Bun). A restructuring that unified scripts, caching, releases, web apps, extensions with shared packages and docs living near the code.

**Examples of what changed:**

* Less copy‑paste, more shared libraries.
* One onboarding surface; less context switching.
* Predictable pipelines; fewer flaky builds.

Convincing people to invest in DX in a business is hard because impact isn't always instant. But if you make the case, talk with the right people, show small proofs, and put your name on it, the outcomes are big: throughput rises and the team's day‑to‑day gets calmer. If your company never lets you improve DX, it may not be the right place to grow as a developer.

### Rediredge — a DX‑first redirector for your own domains

#### [Site](https://rediredge.app) • [GitHub](https://github.com/leonardotrapani/rediredge)

**What it is:** a blazing‑fast, self‑hostable domain redirector. Go edge + Next.js control plane. Think "URL shortener," but for **your** domains—without touching DNS every time.

**Why it's DX‑friendly:**

* **One‑command self‑hosting.** Spin up a VPS, clone, `docker compose up -d`.
* **Make devs do devs work.** Marketing or managers can add/change redirects in the web app instead of asking engineers to tinker with CloudFront/DNS.
* **Delegate safely.** Beautiful, simple dashboard for non‑technical teammates; developers keep control of the edge service.
* **Ecosystem**: Of course, it uses a set of tools that shout **dx**

**Setup (self‑host) in 3 steps:**

```bash
# 1) Provision a VPS
# 2) Clone the repo
git clone https://github.com/leonardotrapani/rediredge && cd rediredge
# 3) Run
docker compose up -d
```

Point a CNAME once; manage redirects forever in the UI. That's DX: less back‑and‑forth, more shipping.

***

### Hyprvoice — my computing philosophy, made practical

[Hyprvoice](https://github.com/leonardotrapani/hyprvoice) is voice‑powered typing for Wayland/Hyprland. It exists because my whole approach to computers is DX‑first: Arch, Neovim, tools that respect power users and get out of the way. Hyprvoice was my way to make that environment even smoother.

Read the mindset behind it: [Stop fighting your computer: the OS nightmare](https://leotrapani.com/blog/the-os-nightmare).

**Tiny setup snippet:**

```bash
yay -S hyprvoice-bin
hyprvoice configure
systemctl --user enable --now hyprvoice.service
```

***

## Why DX matters more than it looks

* **It compounds.** Every faster action multiplies across every PR and every teammate.
* **It spreads.** Good DX wins converts—then becomes the new default.
* **It unlocks output.** Happier devs ship more, review more, take more ownership.

***

## If you want to dig deeper

* **Rediredge:** [rediredge.app](https://rediredge.app) • [github.com/leonardotrapani/rediredge](https://github.com/leonardotrapani/rediredge)
* **Hyprvoice:** [github.com/leonardotrapani/hyprvoice](https://github.com/leonardotrapani/hyprvoice)
* **On my system setup mindset:** [The OS nightmare](https://leotrapani.com/blog/the-os-nightmare)
* **Very old (and cringe) Italian DX videos of mine** - most popular on Youtube Italy:
  * Shadcn UI: [https://youtu.be/cEEUvB1LkVo](https://youtu.be/cEEUvB1LkVo)
  * Full‑stack tooling: [https://www.youtube.com/watch?v=VYwCtJ40H\_U\&t](https://www.youtube.com/watch?v=VYwCtJ40H_U\&t)

***

DX isn't an accessory. It's leverage. If you care about ideas becoming products, you should care about the experience of the people turning ideas into code.

If you disagree—or want to trade war stories—reach out. I love this stuff.


Last updated on October 15, 2025

---
title: "Stop fighting your computer: How to solve the OS nightmare"
description: "Tired of losing focus because your OS fights against your workflow? After years of friction with mainstream operating systems, I took control and built my own personalized computing environment. Here's how I eliminated the daily frustrations, customized what mattered most, and created a computer that finally works the way I think"
last_updated: "September 13, 2025"
source: "https://leotrapani.com/blog/the-os-nightmare"
---

# Stop fighting your computer: How to solve the OS nightmare

Tired of losing focus because your OS fights against your workflow? After years of friction with mainstream operating systems, I took control and built my own personalized computing environment. Here's how I eliminated the daily frustrations, customized what mattered most, and created a computer that finally works the way I think

We spend most of our time on a computer. As a software developer, freelancer, and undergrad student, I use mine for everything. And as a busy kid, everything has to be optimized.

## The Pain

Computers haven't felt natural in a while. On macOS, you open a window, lose track of it, resize it, open the things you wanted, memorize gestures someone else decided. Just reading this feels confusing right? But think about your flow when switching between tabs, or doing anything else.

Does that description now look a bit more familiar?

Computers are built to be functional for the average person.
Not for you.

That's the real issue.

To get into a flow state, you need to express your thoughts without thinking about the mechanics. The moment you ask:

> Where is that app?

> How do I open it?

> Do I have to write this that way?

you're not just losing time, you're losing focus. You're out of flow.

That's why [ThePrimeagen](https://www.youtube.com/c/theprimeagen) is so obsessed with Neovim. He **thinks** code and doesn't waste time on how to write or edit it. He just does. And all the optimizations he does for writing code are not so much to gain 20% on typing speed:

He's looking for no friction between what's in your head and how it becomes zeros and ones in memory.

That's the philosophy behind how I approach computers.

### The Current State of Operating Systems

Mainstream operating systems have a single objective: be easy to install, learn, and use for everyone. That's great for most people, but it conflicts with the idea of removing friction for power users.

And because they are closed source, deep customization is limited. We end up leaning on tools like [Raycast](https://www.raycast.com/) just to claw back a little control.

This is where Linux shines.

## The Solution

Linux is open source, and truly customizable. People build distributions, window managers, file managers, audio systems, network interfaces, GUIs, TUIs, and more.

You can practically craft your own OS.

### How to Approach This

If your goal is control and customization, [Arch Linux](https://archlinux.org/) is a strong choice (yes, I use Arch, btw). It lets you choose every component in a safe, practical way, backed by a great community.

Save your configuration, your dotfiles, to Git so you can reproduce your setup quickly.

Real talk: building a full Arch environment is hard and time consuming. You have to balance:

* Time spent building the OS
* Time saved by using it every day

### Omarchy

That's where [Omarchy](https://omarchy.org/) helps.

Omarchy is "an opinionated Arch + Hyprland setup" by DHH.

[David Heinemeier Hansson](https://dhh.dk/) (creator of Ruby on Rails, co-founder of [37signals](https://37signals.com/), Le Mans class-winning driver, and one of my heroes) felt the same pain and wanted to address it. So he built a very opinionated Arch-based setup.

But the opinions themselves aren't what I like most about Omarchy. They are his opinions. The point is to arrive at your own.

### Customization

The real power of Omarchy is customization.

DHH and the open source contributors built a framework that captures ideas in a reproducible, shareable environment.

So here's what I did, and what you should do if this resonates: fork it.

By forking, you keep Omarchy's framework for creating a custom OS and decide how everything works.

Here's [my fork](https://github.com/leonardotrapani/omarchy) if you want a look.

I want to change a keybind? I change it.
I want to tweak the look? I tweak it.
I want to add a tool I always use? I add it.
I want to switch the Wi-Fi interface? I switch it.

You can basically merge other Open Source projects and components together to create what you need.

Or if they don't exist, or they don't do it the way you want it, you can create your own.

For example, I needed a way to have powerful dictation, and I knew other people needed it, so I created [Hyprvoice](https://github.com/leonardotrapani/hyprvoice). I wrote a [blog post](/blog/hyprvoice) about it if you are interested.

Omarchy isn't great because of its default setup, even if many people use it that way.

It's great because it makes crafting your own opinionated setup easy.

***

Now when I sit down to work, my computer gets out of my way. No more hunting for apps, no more fighting with windows, no more breaking flow state over interface friction.

That's what's possible when you stop accepting someone else's opinions about how you should compute.


Last updated on September 13, 2025

---
title: "Hyprvoice: Voice-Powered Typing for the Modern Linux Desktop"
description: "Hyprvoice is a lightweight, Wayland-native daemon that transforms your speech into instant text input across any application without the overhead of heavy software or legacy X11 hacks."
last_updated: "August 15, 2025"
source: "https://leotrapani.com/blog/hyprvoice"
---

# Hyprvoice: Voice-Powered Typing for the Modern Linux Desktop

Hyprvoice is a lightweight, Wayland-native daemon that transforms your speech into instant text input across any application without the overhead of heavy software or legacy X11 hacks.

Voice-to-text on the Linux desktop has always been a second-class citizen.

If you want to type with your voice, you usually have to choose between two equally frustrating paths:

1. **The Bloated Path:** A heavy Electron app that eats a gig of RAM just to listen to your microphone.
2. **The Legacy Path:** A fragile chain of X11 scripts that break the second you move to a modern Wayland compositor.

I was tired of the friction. I wanted something that felt invisible: a tool that didn't live in a window, but in my workflow. So I built **Hyprvoice**.

## The Invisible Workflow

The best tools are the ones you forget exist.

When I’m in a deep-work loop, the last thing I want to do is grab my mouse to click a "Record" button. With Hyprvoice, I tapped `Super+R`, said "create a new React component called UserProfile," tapped `Super+R` again, and watched the code appear at my cursor.

That's the core philosophy: **Voice is an input pipeline, not an application.**

## Why Wayland Native?

Most existing solutions rely on `XTest` or other legacy protocols. On a modern stack (like Hyprland or GNOME on Wayland), these require XWayland translation layers that are prone to latency and weird focus bugs.

Hyprvoice is built from the ground up for the modern Linux stack:

* **Go-powered Daemon:** A lightweight control plane that stays resident in memory with almost zero overhead.
* **PipeWire Integration:** Direct, low-latency audio capture without the PulseAudio baggage.
* **Direct Injection:** It hooks into the native Wayland input flow using `wtype` and `ydotool`.

## Under the Hood: The Secret Sauce

The project is structured around a state-machine pipeline. This ensures that the audio frames are handled efficiently without leaking memory or getting stuck in a recording loop.

Because this entire pipeline is multithreaded through goroutines, it needs to be secure and predictable. The secret sauce behind that calm? Go's context everywhere. Every long-running operation and external call carries a deadline or cancel.

Toggle again and the entire pipeline unwinds. A timeout fires and recording stops; sockets close; goroutines exit. On shutdown, WaitGroups drain, contexts propagate, and the system cleans up predictably. Here's the pattern you'll find all over the codebase:

```go
runCtx, cancel := context.WithTimeout(ctx, p.config.Recording.Timeout)
p.setCancel(cancel)
 
p.wg.Add(1)
go p.run(runCtx)
```

## Modular and Fully Configurable

Everything in the pipeline is exposed to the user.

You can swap transcription "brains" without a restart—switching from OpenAI to Groq for sub-second latency.

Beyond the backend, you can fine-tune audio sample rates, customize desktop notification messages for every state, and manage a priority-based injection fallback chain.

This ensures that (in the following example) if direct typing via `wtype` or `ydotool` can't reach a specific application, the clipboard path quietly takes over and restores your original content in the background, keeping the system stable without you ever needing to touch the code.

```toml
[transcription]
provider = "groq-transcription" # Also supports "openai" and "groq-translation"
api_key = "gsk_..." 
language = "en"
model = "whisper-large-v3-turbo"

[injection]
backends = ["ydotool", "wtype", "clipboard"]
ydotool_timeout = "5s"
wtype_timeout = "5s"
clipboard_timeout = "3s"
```

and more...

## Installation and Contributing

Hyprvoice is open-source and currently in Beta. If you're on Arch, you can install it directly from the AUR:

```bash
yay -S hyprvoice-bin
```

I'm looking for contributors to help with the `whisper.cpp` implementation and much more. If you want to help make voice typing on Linux a first-class experience, check out the repository.

[View on GitHub](https://github.com/leonardotrapani/hyprvoice)


Last updated on August 15, 2025