Tolari is a local-first desktop productivity app for students. No account, no sync, no server, everything just lives on your machine. I built it with Wails, which lets you write a Go backend and a React frontend that talk to each other through a generated bindings layer, and the whole thing compiles down to a single native binary.
The idea behind it is that three well-researched study techniques, spaced repetition, Kanban planning, and the Pomodoro technique, work better together than as three separate tools you have to switch between. Tolari just puts them all in one place.
Stack
The backend is Go, exposed to the frontend through Wails bindings. The frontend is React with Vite. Flashcard data lives in SQLite, and Kanban state gets persisted as structured data files on disk. It ships as a single executable, so there's no installer needed on most platforms.
Wails works by embedding a WebView inside a native window and injecting a JS bindings layer that maps directly to exported Go functions. From React's side, calling a Go function looks just like calling an async JS function:
// Go — exported to the frontend automatically by Wails
func (a *App) GetReviewCards() ([]Flashcard, error) {
rows, err := a.db.Query(`
SELECT * FROM flashcards
WHERE review_date < CURRENT_TIMESTAMP;
`)
...
}
// React — generated binding, called like any async function
const cards = await GetReviewCards();
No REST API, no HTTP, no serialization boilerplate to write. Wails just handles the bridge for you.
Spaced repetition flashcards
The flashcard system is modeled after Anki. Cards get reviewed on a schedule based on how well you recalled them, not just how recently you studied them. The core idea is that memory decays in a fairly predictable way, and the best time to review something is right before you'd forget it. Review too early and you're wasting time. Review too late and you're basically relearning it from scratch.
Each card has an interval (days until the next review) and an ease factor (a multiplier that grows when you recall well and shrinks when you struggle). After every review you rate how well you remembered it, and the next interval gets calculated like this:
FUNCTION review(flashcard, difficulty):
// Get the current time
current_time = GET_CURRENT_TIME()
// If response was incorrect/poor (difficulty < 3)
IF difficulty < 3 THEN
flashcard.repetitions = 0
flashcard.interval = 1
// If response was correct (difficulty >= 3)
ELSE
// Calculate new Easiness Factor (EF) -> from the SM2
// algorithm
flashcard.easiness = flashcard.easiness + 0.1 -
(5 - difficulty) * (0.08 + (5 - difficulty) * 0.02)
// Keep Easiness Factor from dropping
// below the minimum threshold of 1.3
IF flashcard.easiness < 1.3 THEN
flashcard.easiness = 1.3
END IF
// Increment repetitions and determine the next interval
flashcard.repetitions = flashcard.repetitions + 1
CHOOSE CASE flashcard.repetitions:
CASE 1:
flashcard.interval = 1
CASE 2:
flashcard.interval = 6
DEFAULT:
flashcard.interval = ROUND(
flashcard.interval * flashcard.easiness
)
END CHOOSE
END IF
// Calculate and save the next review date
flashcard.review_date = SERIALIZE_DATE(
current_time + flashcard.interval DAYS
)
END FUNCTION
Cards are stored in SQLite along with their next due date. When the app starts up, it queries for everything due that day and surfaces it for review. Once you've gotten through your due cards, you're done. There's no pressure to do more than that.
Kanban board
The Kanban board is for planning study sessions and tracking tasks across subjects. Columns represent stages (To Do, In Progress, Done), and cards move between them as work gets done.
State here is saved as structured data files on disk instead of going into SQLite. The board has its own file. The Go backend handles all the reads and writes, and the React frontend just sends the entire updated board state on every change instead of patching individual fields. It's simple, and easy to reason about.
Pomodoro timer
The Pomodoro timer is what ties the other two features together. The technique itself is simple: work in focused 25-minute blocks, take a short break, repeat. After four blocks you take a longer break. That structure helps fight the tendency to fall into unfocused marathon study sessions.
Why local-first
Most productivity apps want you to make an account. That means your data sits on someone else's server, the app stops working without internet, and you're one canceled subscription away from losing access to everything. For a study tool that students use daily, often on a school network, on a plane, or just without wanting to hand over an email address, local-first felt like the right default.
SQLite and flat files on disk mean your data is actually yours. You can read it, back it up, and it isn't going anywhere.