Sitemap

I Open-Sourced Hazlo: A Voice-to-Notion Task Capture App

16 min readJun 17, 2026

--

Built with React, Vite, Vercel, and Notion API Capture

Tasks do not always get lost because we are careless.

Sometimes they get lost because capturing them takes too much friction.

You remember something while coding.
You think of a follow-up after a meeting.
You plan something while walking.
You suddenly remember a small task while switching between projects.

Then the usual flow starts.

Open Notion.
Find the right page or database.
Type the task.
Add some context.
Maybe add a status.
Maybe organize it later.

Most of the time, that is too much work for a small thought.

So the thought stays in your head.

And then it disappears.

That was the problem I wanted to solve with Hazlo.

Hazlo is not trying to replace Notion. It is trying to make Notion easier to feed.

The idea is simple:

Speak first. Organize later.

Links

Live Demo: https://hazlo-opensource.vercel.app
GitHub Repo:
https://github.com/YellankiKaushik/Hazlo-OpenSource
Technical Documentation:
https://github.com/YellankiKaushik/Hazlo-OpenSource/blob/main/docs/TECHNICAL_DOCUMENTATION.md

What I Built

Hazlo is an open-source, voice-first productivity app that lets users speak their thoughts, extract likely tasks locally, store entries in the browser, and sync transcripts plus tasks into Notion through a Vercel serverless backend.

In normal language, Hazlo lets me speak something like:

I need to finish the landing page, send the update to Kaushik, and review the Vercel deployment.

Then it saves the full transcript, extracts likely tasks, and sends everything into my Notion database.

The current version is intentionally simple.

It uses React, Vite, and TypeScript for the frontend. It uses the browser Web Speech API for voice capture. It uses Zustand and localStorage for local state and persistence. It uses a rule-based task extraction utility to find likely tasks. It uses a Vercel serverless function to safely sync entries to Notion. And it uses Vitest for focused unit testing.

It is now public, deployed, documented, and open source.

You can try it here:

https://hazlo-opensource.vercel.app

And you can fork the code here:

https://github.com/YellankiKaushik/Hazlo-OpenSource

Why I Built Hazlo

I built Hazlo because I wanted a faster way to capture my work, thoughts, tasks, and daily planning without losing them.

I use Notion, but I did not want to manually open Notion every time I remembered a task.

Not every thought needs a full productivity workflow at the moment it appears.

Sometimes I just want to say it out loud, save the raw context, and let the system create a basic structure for me.

That is the main idea behind Hazlo:

Capture first. Organize later.

I did not build Hazlo as a full SaaS product.
I did not build it as a multi-user workspace.
I did not build it as an AI agent.

I built it as a small, understandable, forkable productivity system.

Something that takes a thought from my voice and moves it into a place where I can actually act on it later.

The Problem with Normal Task Capture

Most productivity tools are powerful once the task is already inside them.

The real problem is the moment before that.

You remember something while coding, but opening another app breaks your flow.

You finish a meeting and have three action items in your head, but you delay writing them down.

You are walking or thinking, and typing feels slower than speaking.

You are switching between projects, and a small follow-up appears in your mind for only a few seconds.

The normal workflow expects you to type and organize before saving.

That works for planned tasks.

But it does not work well for messy thoughts, quick reminders, or spoken planning.

Hazlo tries to reduce friction at the capture layer.

Instead of forcing structure first, it lets me capture the thought first and preserve the context.

The structure can come after.

How Hazlo Works at a High Level

At a high level, Hazlo follows this flow:

Thought
→ Spoken note
→ Browser transcript
→ Local entry
→ Extracted tasks
→ Vercel API route
→ Notion page

Here is what each step means.

First, I think of something I need to remember.

Then I speak it into Hazlo using the microphone button.

The browser converts my speech into a transcript using the Web Speech API.

Hazlo creates a local entry from that transcript.

A local task extraction utility looks for likely tasks using simple rules.

The entry is saved in the browser, so it does not disappear immediately if the sync fails.

Then the frontend sends the entry to a Vercel API route.

The serverless backend validates the request and creates a Notion page using the Notion API.

The final result is a saved Notion page containing the raw transcript and extracted to-do blocks.

Press enter or click to view image in full size
voice-to-Notion concept

Core Features

Voice Capture Using the Browser Web Speech API

Hazlo uses the browser’s native speech recognition support.

When the user clicks the microphone button, the app checks for:

window.SpeechRecognition || window.webkitSpeechRecognition

If the browser supports it, Hazlo starts listening.

The current implementation uses continuous recognition and interim results. That means the transcript can update while the user is still speaking, instead of only appearing at the end.

This keeps the product lightweight because Hazlo does not need to run a custom speech-to-text backend.

There is an important limitation here: browser support varies. In practice, users may need a Chromium-based browser for the best experience.

Transcript Capture

Hazlo stores the full spoken transcript as the source of truth.

This matters because task extraction is never perfect.

Even if the app misses a task or extracts something imperfectly, the raw thought is still preserved.

That was an intentional product decision.

For a personal productivity system, I care more about not losing the thought than creating a perfectly polished task title immediately.

Rule-Based Task Extraction

Hazlo currently uses rule-based task extraction.

It does not call an AI model.
It does not use embeddings.
It does not send the transcript to a task parsing service.

The extractor looks for patterns such as numbered lists, bullet lists, simple “and” separated actions, and short imperative phrases.

For example:

Finish the landing page and send the update to Rahul.

Can become:

- Finish the landing page
- send the update to Rahul

This is simple, fast, local, free to run, and easy to test.

It is also limited.

The extractor does not deeply understand meaning. It may not infer due dates, priorities, projects, or tags. It may also preserve wording that a smarter parser would rewrite.

For the current version, that tradeoff is acceptable because Hazlo is focused on capture first.

Local Browser Persistence

Hazlo stores entries locally in the browser using Zustand persistence and localStorage.

This means that when I create an entry, it is saved locally first.

If Notion sync fails, the entry does not immediately disappear.

That is important because the worst possible failure for a capture tool is losing the thing the user just captured.

The local storage key used by the app is:

hazlo-storage

The app persists entries and rollover state, while runtime voice UI state remains temporary.

Sync Status

Each entry has a sync status.

The main states are:

pending → synced
pending → failed

In the UI, these appear as:

Syncing
Synced
Sync failed

This makes the system more transparent.

The user can see whether the entry is still syncing, has successfully reached Notion, or needs attention.

Retry Failed Sync

If an entry fails to sync, Hazlo keeps it locally and shows a retry option.

Clicking retry sends the same entry through the sync flow again.

This is useful when the failure is caused by a temporary network issue, a missing environment variable, or a Notion configuration problem that the user later fixes.

Notion Sync

Hazlo syncs entries to Notion through a Vercel serverless backend.

The frontend does not call Notion directly.

That is important because the Notion integration token should never be exposed in browser code.

Instead, the frontend sends the entry to:

/api/notion-sync

The backend reads the Notion token and database ID from environment variables, validates the request, and creates a Notion page.

Open-Source Self-Hosting

Hazlo is meant to be forked.

The public repo is not designed so that everyone uses my Notion workspace.

Instead, each person can fork the project, create their own Notion integration, create their own database, add their own Vercel environment variables, and run their own version.

That is the main reason I wanted to open-source it.

Technical Architecture

Hazlo is intentionally small.

I wanted the system to be understandable end to end.

The frontend is built with React, Vite, and TypeScript. Tailwind CSS handles the UI styling. Zustand manages the app state. localStorage keeps entries available in the browser. A local task extractor finds likely tasks from the transcript. A frontend Notion service sends entries to a Vercel API route. The Vercel serverless function then talks to the Notion API.

Here is the architecture:

User speaks
→ Browser Web Speech API
→ React UI
→ Zustand Store
→ localStorage
→ src/services/notion.ts
→ POST /api/notion-sync
→ Vercel Serverless Function
→ Notion API
→ Notion Database
Press enter or click to view image in full size
Architecture diagram

React + Vite + TypeScript Frontend

React powers the interface.

The main UI pieces are focused and small:

VoiceInput
EntryList
EntryCard
TaskItem
Header
Home

Vite handles local development and production builds.

TypeScript helps define the data model clearly. Hazlo has typed entries, tasks, and sync statuses, which makes the state and service code easier to reason about.

Tailwind CSS UI

Tailwind CSS is used for styling.

For a compact app like Hazlo, utility-first styling works well because most UI components are small and component-driven.

The goal is not to create a heavy design system.

The goal is to keep the interface clear enough that the user can quickly capture a thought.

Zustand Store

Zustand owns the main app state.

It stores entries, voice state, task actions, sync actions, and rollover behavior.

The store is also responsible for creating entries, extracting tasks, starting background sync, updating sync status, and retrying failed sync.

This keeps the app lightweight without needing a larger state management framework.

localStorage Persistence

Hazlo persists selected state into localStorage.

The important part is that local entries survive page reloads.

Only selected state is persisted. Runtime state like the current recording status or live transcript is not meant to be permanently stored.

Task Extractor Utility

The task extraction logic lives in:

src/utils/taskExtractor.ts

This utility is deterministic and rule-based.

That means it is easy to inspect, modify, and test.

If someone forks Hazlo and wants different extraction behavior, this is one of the best places to start.

Frontend Notion Sync Client

The frontend sync client lives in:

src/services/notion.ts

Its job is to take a local entry and send it safely to the backend route.

It sends a request to:

/api/notion-sync

It also includes headers such as:

Content-Type
X-API-Secret
X-Idempotency-Key

The client handles safe error messages and retries selected temporary failures.

Vercel Serverless Backend

The backend route lives in:

api/notion-sync.js

Vercel deploys this as a serverless function.

This function exists for one main reason:

The Notion token must stay on the server side.

The backend validates the request, checks the API secret, reads Notion environment variables, creates the Notion page, and returns a safe response to the frontend.

Notion API

The Notion API is used to create pages inside the user’s configured database.

Each Hazlo entry becomes a Notion page.

The transcript is saved as the main context, and extracted tasks become Notion to-do blocks.

The required Notion database properties are simple:

Raw Speech → Title
Status → Status

This keeps the first version easy to set up and easy to understand.

Voice-to-Task Workflow

The main user flow starts with the microphone.

The user clicks the mic button.

The browser asks for microphone permission if permission has not already been granted.

Once recording starts, the Web Speech API converts speech into transcript text.

Hazlo displays the transcript while the user speaks.

When recording stops, Hazlo creates a new entry from the transcript.

Then the local task extractor runs.

The entry is saved locally first.

After that, Notion sync starts in the background.

Here is an example.

Input:

I need to finish the landing page, send the update to Rahul, and review the Vercel deployment.

Possible extracted output:

- finish the landing page
- send the update to Rahul
- review the Vercel deployment

The raw transcript still remains important.

Task extraction can be helpful, but the transcript is the full source of truth.

Get Kaushik Yellanki’s stories in your inbox

Join Medium for free to get updates from this writer.

That gives Hazlo a practical balance.

It extracts structure when it can, but it does not throw away the original thought.

Press enter or click to view image in full size
Voice-to-Notion workflow

Notion Sync Workflow

The Notion sync flow is one of the most important parts of Hazlo.

A common mistake in projects like this is calling the Notion API directly from the frontend.

That would expose the Notion token to anyone who can inspect the frontend bundle or network requests.

Hazlo avoids that by using a serverless backend.

The frontend sends a request to:

/api/notion-sync

The backend then validates the request.

It checks that the method is allowed, the API secret is correct, the request body is valid, rawSpeech is present, the Notion environment variables exist, and the tasks are in a valid shape.

Then it creates a Notion page.

The transcript becomes a paragraph block.

The extracted tasks become Notion to_do blocks.

The backend uses these server-side values:

NOTION_TOKEN=your_notion_token
NOTION_DATABASE_ID=your_database_id
API_SECRET=your_random_secret

The frontend sends:

VITE_API_SECRET=your_same_random_secret

API_SECRET and VITE_API_SECRET must match.

But this should be understood clearly:

This is lightweight protection, not full user authentication.

Because VITE_API_SECRET is included in the frontend build, it should not be treated as a strong private credential in a public multi-user SaaS.

For a personal self-hosted tool, this model is simple and useful.

For a production SaaS, Hazlo would need proper authentication, authorization, per-user Notion connections, and stronger abuse prevention.

Environment Variables and Security

Hazlo needs environment variables to connect to Notion and protect the backend route.

For a normal Vercel production deployment, the main variables are:

NOTION_TOKEN=your_notion_token
NOTION_DATABASE_ID=your_database_id
API_SECRET=your_random_secret
VITE_API_SECRET=your_same_random_secret

Here is what each variable does.

NOTION_TOKEN is the private Notion integration token. This must stay server-side.

NOTION_DATABASE_ID tells the backend which Notion database to create pages in.

API_SECRET is the server-side secret expected by the backend.

VITE_API_SECRET is sent by the frontend and must match API_SECRET.

Again, VITE_API_SECRET is not strong authentication because frontend variables are visible in the browser build.

There is also an optional variable:

VITE_API_BASE_URL=

VITE_API_BASE_URL is optional and mainly useful for local development when the frontend needs to call a separately deployed backend. In normal Vercel production deployment, it should usually be omitted.

Do not commit real secrets.

Never commit:

.env
.env.local
Notion tokens
API secrets
Vercel tokens
Real database IDs

The repo includes an .env.example file for placeholders.

Use that as a template, but keep real values in local or platform environment settings.

Press enter or click to view image in full size
Env variables and security visual

How to Fork and Deploy Hazlo

Hazlo is designed so that anyone can fork it and run their own personal version.

The original project owner does not need access to your Notion token, database, or entries.

Your data should go to your own Notion workspace when your deployment is configured correctly.

Here is the full setup flow.

Step 1: Fork the GitHub Repo

Open the repo:

https://github.com/YellankiKaushik/Hazlo-OpenSource

Click Fork on GitHub.

Create a copy under your own GitHub account.

Do not add secrets directly to the repository.

Step 2: Clone Your Fork

Replace YOUR_USERNAME with your GitHub username:

git clone https://github.com/YOUR_USERNAME/Hazlo-OpenSource.git
cd Hazlo-OpenSource

Step 3: Install Dependencies

Hazlo uses pnpm.

Run:

pnpm install --frozen-lockfile

This installs the project dependencies using the lockfile.

Step 4: Create a Notion Integration

Go to Notion’s integrations page and create an internal integration.

Copy the integration token.

This token becomes:

NOTION_TOKEN=your_notion_token

Keep this token private.

Do not paste it in frontend code.

Do not commit it to GitHub.

Do not share it in screenshots.

Step 5: Create a Notion Database

Create a Notion database where Hazlo will create pages.

The database needs these required properties:

Raw Speech → Title
Status → Status

For the Status property, you can use values like:

Not started
In progress
Done

Hazlo currently sends new entries as Not started.

Step 6: Share the Notion Database with the Integration

This step is easy to miss.

Creating the integration is not enough.

You must open the Notion database and share it with the integration.

If the database is not shared with the integration, the backend will not be able to create pages in it.

Step 7: Copy the Database ID

Copy the database ID from the Notion database URL.

This value becomes:

NOTION_DATABASE_ID=your_database_id

Keep the real value private.

Step 8: Create a Vercel Project

Go to Vercel and import your forked GitHub repository.

Use these settings:

Framework: Vite
Install Command: pnpm install --frozen-lockfile
Build Command: pnpm build
Output Directory: dist

Vercel should detect most of this automatically, but it is good to verify the settings.

Step 9: Add Environment Variables in Vercel

In Vercel Project Settings, add:

NOTION_TOKEN=your_notion_token
NOTION_DATABASE_ID=your_database_id
API_SECRET=your_random_secret
VITE_API_SECRET=your_same_random_secret

Make sure:

API_SECRET == VITE_API_SECRET

They must match.

VITE_API_BASE_URL is optional and mainly useful for local development when the frontend needs to call a separately deployed backend. In normal Vercel production deployment, it should usually be omitted.

After changing any VITE_ variable, redeploy the app so the frontend bundle receives the new value.

Step 10: Deploy

Deploy the project from Vercel.

After the build completes, open the deployed URL.

Step 11: Test the Voice-to-Notion Flow

Use this test flow:

Speak → Entry appears → Syncing → Synced → Notion page created

A good test phrase is:

Finish the README update and test the Notion deployment.

Expected result:

The entry appears locally in Hazlo.

The sync status changes from Syncing to Synced.

A new page appears in your Notion database.

The transcript is saved.

If the extractor finds tasks, they appear as Notion to-do blocks.

Press enter or click to view image in full size
Fork + deploy setup flow

Testing and Verification

Hazlo uses Vitest for focused unit tests.

The tests cover the task extraction utility and the frontend Notion sync service.

The Notion sync service tests mock fetch, so they do not call the real Notion API and do not require real Notion credentials.

Before shipping changes, these commands are useful:

pnpm test
pnpm typecheck
pnpm build
git diff --check

pnpm test runs the Vitest test suite.

pnpm typecheck checks the TypeScript code.

pnpm build verifies that the production build works.

git diff --check catches whitespace issues before committing.

The current testing approach is intentionally practical.

It does not test every possible browser speech behavior, and it does not do real Notion integration tests by default.

But it does cover the parts that are easiest to break during development: task extraction and sync request behavior.

How I Use Hazlo Daily

Hazlo is useful for small moments where I want to capture something quickly without opening a full planning system.

One use case is morning planning.

Instead of starting the day by manually typing everything into Notion, I can speak a rough plan and let Hazlo save the transcript and extract obvious tasks.

Another use case is coding.

When I am working on a feature, I often remember small follow-ups: update docs, test a route, fix a deployment setting, send someone an update. Hazlo helps capture those without fully breaking focus.

After meetings, Hazlo can be used as a quick action-item dump.

Instead of trying to remember everything later, I can speak the immediate follow-ups while they are still fresh.

For freelance or client work, it can help log small updates, invoices, reviews, deployment checks, and messages to send.

It can also work as a simple daily reflection tool.

At the end of the day, I can speak what I worked on, what is pending, and what I need to continue tomorrow.

The larger idea is not complicated.

Hazlo is useful when the thought is clear enough to speak but not yet organized enough to manually structure.

Press enter or click to view image in full size
Daily productivity use case

Limitations

Hazlo has limitations, and I want to be clear about them.

Voice support depends on the browser’s Web Speech API.

That means the experience can vary across browsers and devices.

Microphone permission is required.

If the browser does not support SpeechRecognition or webkitSpeechRecognition, the voice input will not work.

Task extraction is rule-based.

It can extract obvious tasks from numbered lists, bullet lists, simple “and” phrases, and short imperative statements.

But it does not deeply understand natural language.

It does not currently infer due dates, priorities, tags, projects, or assignees.

It is not full AI extraction yet.

Hazlo also does not have user accounts.

The API secret model is lightweight protection, not a full authentication system.

It is useful for personal self-hosted deployments, but it is not enough for a public multi-user SaaS.

localStorage is also not a permanent backup.

Entries can be cleared if the user clears browser data.

Notion is the longer-term workspace once sync succeeds.

Another current limitation is that local task edits after sync do not update the already-created Notion page yet.

If you toggle or delete a local task after the first sync, that change stays local in the current version.

Hazlo is not trying to hide these limitations.

It is a working open-source foundation that can be improved.

Future Improvements

There are many realistic improvements that can make Hazlo more useful.

The first major improvement would be stronger authentication.

If Hazlo becomes more than a personal self-hosted tool, it needs proper user accounts, authorization, and per-user Notion connections.

Another improvement would be AI-based task extraction.

The current rule-based system is simple and reliable, but a future extraction layer could better understand natural language, clean up task wording, detect due dates, infer priorities, and support richer structure.

Other possible improvements include reminders, calendar integration, recurring tasks, and a better Notion database schema.

Mobile and PWA improvements would also make sense because voice capture is especially useful away from a desktop.

Multi-workspace support could help people separate personal tasks, client work, study notes, and project updates.

An onboarding wizard would make setup easier for non-technical users.

Another useful improvement would be optional Notion updates after local task edits, so changes made in Hazlo can sync back to the already-created Notion page.

The current version is small on purpose.

But the workflow has room to grow.

Press enter or click to view image in full size
Future roadmap

Final Impact

Hazlo is not trying to be the biggest productivity app.

It is a small, understandable, forkable productivity system that reduces the friction between thinking of a task and saving it somewhere useful.

For me, it started as a personal problem.

I wanted a faster way to capture my work, thoughts, and tasks without manually opening Notion every time.

I wanted to speak first and organize later.

Now Hazlo is open source.

Anyone can fork it, inspect the code, change the task extraction logic, connect their own Notion workspace, deploy it on Vercel, and build their own voice-to-Notion workflow.

That is the part I like most.

It is not locked behind my setup.

It is not dependent on my Notion workspace.

It is a small system that others can understand and adapt.

Final Links

Live Demo: https://hazlo-opensource.vercel.app

GitHub Repo: https://github.com/YellankiKaushik/Hazlo-OpenSource

Technical Documentation: https://github.com/YellankiKaushik/Hazlo-OpenSource/blob/main/docs/TECHNICAL_DOCUMENTATION.md

Final CTA

Try the live demo.
Fork the GitHub repo.
Read the technical documentation.
Build your own version.

Press enter or click to view image in full size
Open-source impact visual

--

--

Kaushik Yellanki
Kaushik Yellanki

Written by Kaushik Yellanki

AI Builder & Software Developer building practical AI, web, and product projects while sharing the process behind turning ideas into working software.