BapsOS: Windows 98, rebuilt in a browser tab
A Windows 98 simulator with a shell called Fen98 and 27 working applications, running on a Patil Pentium II at 400 MHz with 128.0 MB of RAM.

- 27
- working applications
Context
BapsOS is a Windows 98 simulator that runs in a browser tab. It has a named shell, Fen98, a full boot sequence, a shutdown sequence, and 27 applications that actually work rather than opening a picture of themselves.
The System Properties dialog reports a Patil Pentium II at 400 MHz with 128.0 MB of RAM. None of that hardware exists. The dialog is confident about it.
The problem
Retro desktop recreations on the web are almost always screenshots with hover states. You click Start, a menu appears, you click Minesweeper, and you get a placeholder or a link to somebody else’s Minesweeper. The illusion collapses on the second click, because the first click is the only one anybody tested.
What made a desktop operating system feel like a place was not the wallpaper. It was that everything was live at once. You could have a document open behind a game behind a control panel, drag a window half off the screen, minimise it, and find it in the taskbar where you left it. The depth was the product.
So the question was whether I could build the depth rather than the screenshot, alone, in a browser.
My role
Solo. Product design and engineering, all 27 apps, the shell, the window manager, the sound design, the fake hardware specs.
The design half is not window chrome pixel-matching, though there is some of that. It is the systems design underneath: what a window is, what an application is, how the shell launches one, how state persists, and how all of that stays consistent across 27 things built over months. Get that wrong and the twentieth app is a rewrite of the first.
Approach
I built the window manager before I built any application, and I refused to write an app until a window could be opened, focused, dragged, resized, minimised, maximised, restored and closed correctly. That is unglamorous work with nothing to show for weeks, and it is the entire reason the project finished.
Once a window is a solved primitive, an application is just content plus a manifest. Minesweeper, Tetris, Snake, a TipTap rich-text editor, a spreadsheet with working formulas, an AI chat, Paint, a webcam app, Task Manager, Control Panel and the rest all plug into the same contract. Adding the twenty-seventh took an afternoon. The first took a month.
The fake hardware and the boot sequence are the same decision as the window manager, just at the level of tone. A simulator that boots instantly is a website. A simulator that spends a few seconds counting imaginary memory is a machine.
Building it
TanStack Start with React 19, built by Vite, deployed to Cloudflare. Everything runs client side. There is no server, no account and no database, which is what lets it cost nothing to host and load fast from the edge.
Zustand holds the entire system state and persists it to localStorage. That is the piece that makes it feel like an OS rather than a page. Open windows, their positions, their z-order, your Paint canvas, your document, your Control Panel settings and your wallpaper all survive a refresh. Close the tab mid-Tetris and come back to the same board. The persistence layer is versioned, because a schema change would otherwise deserialise into garbage on every returning visitor.
The window manager is a single z-order stack with focus as derived state rather than a flag stored per window, which removed a whole category of bugs where two windows both believed they were focused. Dragging and resizing go through @use-gesture, with pointer events rather than mouse events so a phone works. Framer Motion animates window transitions, minimise and restore. Radix UI provides the menus and dialogs underneath the retro skin, which is the least period-accurate decision in the project and the reason the Start menu is keyboard navigable and screen-reader sane. A 1998 interface with 2026 accessibility primitives is a trade I would make again.
The applications carry their own weight. TipTap is the editor. The spreadsheet has a real formula parser with cell references and dependency-ordered recalculation, because a spreadsheet that does not compute is a table. The AI chat streams tokens into a period-correct text box and is rate limited, because an unauthenticated public chat window is a bill waiting to happen. The webcam app uses getUserMedia locally and uploads nothing. Task Manager reports processes that map to open windows, so killing one closes it.
use-sound handles the audio: startup, shutdown, error chimes, clicks. Lucide covers icons with no period equivalent. Microsoft Clarity is the only analytics, and there is a pleasing symmetry in Microsoft telling me how people use my Windows 98.
Outcome
It is live at os.bapspatil.com with all 27 applications working. People find it, lose an hour, and tell me they had that exact machine.
It also became my most reusable piece of engineering. The window manager, the persistence layer and the app contract are parts I keep returning to.
What I’d do differently
I shipped Zustand persistence without a migration path and had to add versioning after real people had real state. That should have been in the first commit.
I would also have written the app manifest contract as a type before building app number two, rather than extracting it around app number eight. The middle apps still carry the shape of that transition.




