- apps/root is the sole Vite build entry point; the four modules are library-only packages bundled together via workspace symlinks, and the modules themselves have no vite.config
- Backends are chosen per module: to-do/habit use Firestore, ebook uses Workers + D1 + R2, resignation stamp uses Workers + Workers AI
- Every endpoint of the ebook Worker API requires a Firebase ID token (Bearer), achieving cross-cloud identity verification
Table of Contents
a920604a Labs is a pnpm monorepo that integrates four independent daily tools into a single repo: a to-do list, a habit tracker, an ebook reader, and a resignation stamp collector. The technical backbone is React 19 + TypeScript + Chakra UI + Firebase + Cloudflare, with the goal of making shared logic (authentication, UI components) genuinely reusable rather than reimplemented in each side project.
The design focus of the whole project isn’t “lots of features” but how the monorepo is carved up: how to let four SPAs share the same auth and ui while still developing independently; and how to flexibly mix and match Firestore or Cloudflare backend services according to each module’s actual needs.
How the Monorepo Is Carved Up: A Single Build Entry Point + Library-Only Modules
The repo is split into three layers: apps/, packages/, and workers/.
a920604a-labs/
├── apps/
│ ├── root/ # SPA entry (route assembly + HubPage)
│ ├── ebook-reader/ # @a920604a/ebook-reader
│ ├── habit-tracker/ # @a920604a/habit-tracker
│ ├── resign-stamp/ # @a920604a/resign-stamp
│ └── to-do-list/ # @a920604a/to-do-list
├── packages/
│ ├── auth/ # @a920604a/auth (Firebase Auth)
│ └── ui/ # @a920604a/ui (GlobalShell, LoginPage, theme)
└── workers/
└── ebook-api/ # Cloudflare Worker + D1 (ebook API)
The key design decision is: apps/root is the sole build entry point for the entire SPA. Its code is extremely minimal, doing just three things—
| File | Responsibility |
|---|---|
src/main.tsx | Calls Firebase initFirebase(), mounts ChakraProvider + AuthProvider |
src/App.tsx | BrowserRouter, MODULES navigation config, <GlobalShell>, four module Routes |
src/pages/HubPage.tsx | Home launch page (time-of-day greeting + four module cards) |
The four feature modules are library-only workspace packages: each apps/{module} exports only a single ./src/index.ts (export { default } from './App'), and has no vite.config.ts of its own. They are bundled all at once by apps/root’s Vite via pnpm workspace symlinks. In other words, the modules are “assembled” into root rather than being built independently and stitched together afterward. This lets the four tools share the same build config and the same set of Providers, while still developing their own sub-routes, components, and hooks independently within their own directories.
Two Shared Packages: auth and ui
Authentication is consolidated in @a920604a/auth, which exposes only a handful of APIs:
initFirebase(config) // Initialize the Firebase App (idempotent)
getFirebaseAuth() // Get the Auth instance
getFirebaseFirestore() // Get the Firestore instance
AuthProvider // React Context Provider
useAuth() // → { user, loading, signInWithGoogle, logout }
Any module that needs login state just uses useAuth()—no need to touch the Firebase SDK directly.
UI is consolidated in @a920604a/ui, centered on GlobalShell—a sidebar shell modeled after the macOS HIG. Its navigation is entirely controlled by the modules prop passed in by the caller, with no hardcoded routes:
interface SidebarModule {
path: string
label: string
icon?: ReactNode
subItems?: { label: string; path: string }[]
exact?: boolean
}
Presentation details of GlobalShell: on desktop, a fixed 240px Sidebar on the left, with the Topbar using backdrop-filter for a frosted-glass effect; on mobile, it collapses into a Hamburger that opens a left-sliding Drawer; the active state is a filled rounded rect à la macOS Finder. The package also exports LoginPage, AppShell, NavBar, and a Chakra-extended theme (brand palette + Noto Sans TC).
The Four Feature Modules
- 📝 To-Do List: Real-time Firestore sync, with due-date alerts, tag categories (work / study / personal / other), three views (list / stats / calendar), and rich-text notes via Tiptap.
- ✅ Habit Tracker: Daily check-ins, streak counts, achievement badges (data stored in Firestore). The stats page uses Recharts + Chart.js to draw weekly/monthly check-in rate line charts, heatmaps, and bar charts, plus browser notification reminders and PDF export via pdf-lib.
- 📚 Ebook Reader: After a PDF is uploaded, it’s cached locally in IndexedDB and simultaneously synced to a Cloudflare Worker’s D1. The reader uses
@react-pdf-viewer, remembers reading progress, and provides a categorized library and a pie-chart stats view. - 🏮 Resignation Stamp: A 100-cell stamp grid (click to stamp and enter a reason), a progress bar, achievement badges, and a daily maxim; reasons can be searched / sorted / exported as .txt, and pdf-lib + fontkit generate a PDF resignation-stamp certificate in one click.
Dual-Cloud Architecture: Pick a Backend per Module
What’s most worth documenting about this project is that it doesn’t force all modules onto the same backend, but mixes and matches by need:
graph LR
User["User"] --> FE["apps/root SPA<br/>Cloudflare Pages"]
FE -->|"Google Sign-In"| FAuth["Firebase Auth"]
FE -->|"Firestore SDK"| FS[("Firestore<br/>to-do / habit")]
FE -->|"Bearer ID token"| EBK["Worker: ebook-api"]
FE --> RSG["Worker: resign-api<br/>Workers AI"]
EBK -->|"verify ID token"| FAuth
EBK --> D1[("D1: ebook-db")]
EBK --> R2[("R2: ebook-pdfs")]
The Cloudflare services each module actually uses:
| Module | Pages | Workers | D1 | R2 |
|---|---|---|---|---|
| Ebook Reader | ✓ | ✓ | ✓ | ✓ |
| Resignation Stamp | ✓ | ✓ | - | - |
| To-Do List | ✓ | - | - | - |
| Habit Tracker | ✓ | - | - | - |
The to-do and habit modules store their data in Firestore, with Cloudflare only doing static hosting; the ebook module leverages the full Workers + D1 + R2 stack; and the resignation stamp uses Workers + Workers AI.
Cross-Cloud Verification: The Worker Trusts the Firebase ID Token
The ebook’s ebook-api Worker is the key integration point of the dual-cloud setup. Every one of its endpoints requires Authorization: Bearer <Firebase ID token>; the Worker verifies the token before authorizing any operation on D1:
| Method | Path | Description |
|---|---|---|
GET | /books?user_id= | Get the book list |
POST | /books | Add a book |
DELETE | /books/:id?user_id= | Delete a book + its progress |
GET | /progress/:bookId?user_id= | Get reading progress |
PUT | /progress/:bookId | Update reading progress |
This way, the Firebase login identity the frontend obtains can be carried all the way to Cloudflare’s backend for authorization—identity issued by Firebase, data stored in D1, with the two clouds each doing their own job.
Tech Stack
| Layer | Technology | Version |
|---|---|---|
| UI framework | React + TypeScript | 19.x / 5.8 |
| Component library | Chakra UI | 2.x |
| Routing | React Router DOM | 7.x |
| Build | Vite + SWC | 6.x |
| Monorepo | pnpm workspaces + NX | 10.x / 20.x |
| Auth | Firebase Auth (Google Sign-In) | 11.x |
| Database | Firebase Firestore | 11.x |
| Local storage | IndexedDB (native API) | — |
| Backend API | Cloudflare Workers | — |
| API DB | Cloudflare D1 (SQLite) | — |
| Deployment | Cloudflare Pages + Workers | — |
| CI/CD | GitHub Actions | — |
Deployment runs through .github/workflows/deploy.yml: after pushing to main, two jobs run automatically—deploy-root builds apps/root and deploys it to Cloudflare Pages (a920604a-labs), while deploy-ebook-api deploys workers/ebook-api to Cloudflare Workers. Both the Firebase and Cloudflare keys are injected from GitHub Secrets.
Conclusion
The real value of a920604a Labs isn’t in the individual features of the four tools, but in how it demonstrates a pragmatic way to carve up a monorepo: a single Vite build entry point + library-only modules makes sharing natural, while picking a backend per module frees you from over-engineering in the name of “uniformity”—simple things go into Firestore, and only what needs relational data and files brings in Workers + D1 + R2. For a personal full-stack playground, this sense of proportion—“share what should be shared, isolate what should be isolated”—is rarer than piling on technology.
References
Answers come from this article only. Click any prompt below or open the chat at the bottom right.
🇺🇸 English
Picture this: you've got four little side projects rattling around in your head. A to-do list. A habit tracker. An ebook reader. And — this one's my favorite — a resignation stamp collector, where you literally stamp a grid every time you fantasize about quitting your job. Four separate apps. And the tempting thing to do is spin up four separate repos, and reimplement login four times, reimplement your UI shell four times, and slowly lose your mind.
a920604a Labs takes the opposite bet. It folds all four into a single pnpm monorepo, and the whole point of the project isn't the features — it's the carving. How do you slice up a monorepo so that four apps genuinely share their authentication and their design system, while still being developed independently? And how do you let each one pick the backend that actually fits it, instead of forcing them all into the same box?
Let's start with the structure. The repo has three layers: apps, packages, and workers. Under apps you've got a folder called root, plus the four feature modules. Under packages you've got two shared libraries — auth and ui. And under workers you've got a Cloudflare Worker for the ebook API.
Now here's the key design decision, and it's a clever one. Apps slash root is the *only* build entry point for the entire single-page app. And root itself is almost embarrassingly thin — it does three things. First, a main file that boots up Firebase and wraps everything in the Chakra UI provider and the auth provider. Second, an App file that sets up routing, defines the navigation config for the modules, drops in the global shell, and wires up the four module routes. And third, a hub page — the home screen, with a little time-of-day greeting and four cards, one per tool. That's it. That's root.
So then what are the four feature modules? Here's the trick: they're library-only. Each one exports a single entry file — basically "here's my App component, take it" — and, crucially, none of them has its own Vite config. They don't build themselves. Root's Vite reaches out through pnpm's workspace symlinks and bundles all four in one shot. So instead of building four apps separately and gluing them together afterward, the modules get *assembled into* root. They all share one build config, one set of providers — but inside their own folders they're free to define their own sub-routes, components, and hooks. Independent development, unified assembly. That's the heart of it.
Okay, the two shared packages. Authentication lives in the auth package, and it deliberately exposes just a handful of things: initialize Firebase, get the auth instance, get the Firestore instance, an auth provider for React, and a hook called useAuth. That hook hands you back the current user, a loading flag, a sign-in-with-Google function, and a logout function. And that's the whole contract. Any module that needs to know who's logged in just calls useAuth — it never touches the raw Firebase SDK. One place owns the messy stuff.
The ui package is built around something called GlobalShell — a sidebar layout modeled on Apple's macOS design guidelines. And the important detail here is that GlobalShell hardcodes nothing. Its entire navigation is driven by a modules prop that the caller passes in — a list of items with a path, a label, an optional icon, optional sub-items, and so on. So the shell doesn't know or care what apps exist; you tell it. On desktop you get a fixed sidebar on the left and a top bar with that frosted-glass blur effect. On mobile it collapses into a hamburger menu that slides a drawer out from the left. And the active item gets that filled, rounded highlight, very Finder-like. The package also ships a login page, a nav bar, and a themed palette with Noto Sans TC as the font.
Quick tour of the four tools themselves. The to-do list syncs in real time through Firestore, with due-date alerts, tag categories, three different views — list, stats, and calendar — and rich-text notes powered by Tiptap. The habit tracker does daily check-ins, streak counting, and achievement badges, with a stats page that draws line charts, heatmaps, and bar charts of your check-in rates, plus browser notifications and PDF export. The ebook reader lets you upload a PDF, caches it locally in the browser's IndexedDB, and simultaneously syncs it up to a Cloudflare Worker backed by D1 — it remembers your reading progress and gives you a categorized library. And the resignation stamp: a hundred-cell grid where each click stamps a cell and logs your reason, with a progress bar, badges, a daily maxim, and — the finishing touch — a one-click generated PDF "resignation certificate." Cathartic software. I respect it.
Now here's the part actually worth writing home about: the backend strategy. Because a920604a Labs does *not* force every module onto the same backend. It mixes and matches based on what each one genuinely needs.
Let me paint the flow. The user hits the single-page app, which is served as static files from Cloudflare Pages. Login goes to Firebase Auth via Google Sign-In. For the to-do and habit modules, the frontend talks directly to Firestore through the SDK — that's where their data lives, and Cloudflare is doing nothing but hosting static files for them. The ebook module is the heavyweight: it calls a Cloudflare Worker, passing along the user's Firebase ID token, and that Worker uses the full stack — D1 for the database, R2 for storing the actual PDF files. And the resignation stamp talks to its own Worker that taps into Workers AI.
So if you laid it out as a grid: all four use Pages for hosting. Ebook and resignation stamp both use Workers. But only the ebook module reaches for D1 and R2. To-do and habit? No Workers, no D1, no R2 — just Firestore and static hosting. Simple things stay simple; only the module that needs relational data and file storage pulls in the heavy machinery.
And that raises an interesting question — if login is handled by Firebase, but the ebook data lives in Cloudflare's D1, how do the two clouds trust each other? That's the real integration point. Every endpoint on the ebook Worker demands an Authorization header carrying the Firebase ID token. Before the Worker touches D1 — before it lists your books, adds one, deletes one, or updates your reading progress — it *verifies* that token against Firebase. So the identity Firebase issued on the frontend gets carried all the way through to Cloudflare's backend for authorization. Firebase issues the identity, D1 stores the data, and each cloud does the job it's best at. That's cross-cloud auth done cleanly.
On the stack itself, briefly: React 19 with TypeScript, Chakra UI for components, React Router 7, Vite with SWC for builds, pnpm workspaces plus NX for the monorepo, Firebase for both auth and Firestore, IndexedDB for local storage, and Cloudflare Workers and D1 on the backend. Deployment runs through GitHub Actions — push to main, and two jobs fire automatically: one builds root and ships it to Cloudflare Pages, the other deploys the ebook Worker. All the secrets come from GitHub Secrets.
So let me leave you with the three ideas actually worth taking away from this.
One: a single build entry point plus library-only modules. Root is the only thing that builds; the four apps are just libraries that get assembled into it. That's what makes sharing feel natural instead of forced — one build config, one set of providers, four independently-developed tools.
Two: pick a backend per module, not per project. Don't over-engineer in the name of uniformity. Simple stuff goes straight into Firestore. Only the module that truly needs relational data and file storage brings in Workers, D1, and R2. Resist the urge to make everything consistent just for consistency's sake.
And three: identity and data don't have to live in the same cloud. Firebase can issue the login, Cloudflare can hold the data, and a verified ID token is the bridge between them.
Honestly, the rarest skill on display here isn't any single piece of technology — it's proportion. Share what should be shared, isolate what should be isolated. That sense of restraint is worth more than piling on tools, and it's exactly what most personal projects are missing.
🇹🇼 中文
a920604a Labs 是一個 pnpm monorepo,把四個原本各自獨立的日常工具塞進同一個 repo 裡:待辦清單、習慣追蹤、電子書閱讀器,還有一個很有梗的「離職集章」。技術主軸是 React 19、TypeScript、Chakra UI,後端則是 Firebase 加 Cloudflare 的雙雲組合。
但這個專案真正有意思的地方,其實不是它功能多,而是它示範了一種務實的 monorepo 切法。核心問題只有兩個:怎麼讓四個獨立的 SPA 共用同一套身份驗證跟 UI,卻又能各自獨立開發?還有,怎麼依每個模組實際的需求,去挑後端服務,而不是一刀切全部用同一套?
先講第一個,monorepo 怎麼切。整個 repo 分成三層:apps、packages、workers。這裡最關鍵的設計是——apps 底下的 root,是整個 SPA「唯一」的建置入口。它的程式碼極度精簡,只做三件事:初始化 Firebase、掛上 Chakra 跟 Auth 的 Provider、然後組裝路由跟一個首頁的 Hub。
那另外四個功能模組呢?它們是所謂的 library-only 套件。什麼意思?就是每個模組的目錄裡,只 export 一個 index,把自己的 App 元件丟出去,本身「沒有」自己的 Vite 設定檔。它們透過 pnpm workspace 的 symlink,交給 root 的 Vite 一次打包。
你可以想像成,這四個工具不是各自建置完再拼起來,而是像零件一樣被「組裝」進 root。好處是四個工具共用同一份建置設定、同一套 Provider,但又能在各自的目錄裡獨立寫自己的 sub-routes、components 跟 hooks。共用的自然共用,隔離的乾淨隔離。
接著是兩個共用套件。身份驗證全部收斂在 auth 這個套件,對外只暴露少少幾個 API——初始化 Firebase、拿 Auth instance、拿 Firestore instance,加上一個 AuthProvider 跟一個 useAuth hook。任何模組要拿登入狀態,只要呼叫 useAuth 就好,裡面直接給你 user、loading,還有 Google 登入跟登出的方法。沒有任何一個模組需要自己去碰 Firebase SDK,這就是收斂的價值。
UI 則收斂在 ui 這個套件,核心是一個叫 GlobalShell 的元件,它是一個仿 macOS 風格的側邊欄外殼。重點是——它的導覽選單完全由呼叫端傳進來的 modules 參數控制,內部「沒有」任何硬編碼的路由。桌面版是左側固定 240 像素的側邊欄,頂欄還用了磨砂玻璃的效果;手機版則收成漢堡選單,點開左滑抽屜。這種「殼由套件提供、內容由呼叫端決定」的設計,就是它能同時服務四個模組的原因。
那四個工具本身簡單帶過。待辦清單走 Firestore 即時同步,有截止日期警示、標籤分類,還有列表、統計、日曆三種視圖。習慣追蹤是每日打卡、連續天數、成就徽章,統計頁用圖表庫畫折線圖、熱力圖跟長條圖。電子書閱讀器比較特別,PDF 上傳後會在瀏覽器的 IndexedDB 做本機快取,同時同步到 Cloudflare。離職集章最好玩,一百格印章格,你點一格蓋一個章、輸入一個離職的理由,最後還能一鍵生成一張 PDF 的「離職集章證明」。
好,重頭戲來了,雙雲架構。這個專案最值得記錄的一點,就是它「沒有」強求所有模組用同一套後端,而是按需求混搭。
你看這個對照就很清楚:待辦跟習慣這兩個,資料存 Firestore,Cloudflare 就只負責當靜態托管而已,連 Worker 都不用。電子書則是火力全開,用上完整的 Workers 加 D1 資料庫加 R2 檔案儲存。離職集章則是用 Workers 搭配 Workers AI。
換句話說,簡單的、只是存存資料的,就直接丟 Firestore;需要關聯式資料跟檔案儲存的,才動用 Cloudflare 那一整套。這種分寸,比硬要統一技術棧要成熟得多。
而這裡有一個技術上的關鍵接點,叫做跨雲驗證。電子書的那個 Worker API,它所有的端點都要求前端帶上一個 Firebase 的 ID token,放在 Authorization 這個 header 裡。Worker 收到之後,會先自己驗證這個 token,確認身份沒問題,才授權你去操作 D1 資料庫——不管是拿書單、新增書、刪除,還是更新閱讀進度。
這個設計漂亮在哪?身份是 Firebase 發的,資料是 Cloudflare D1 存的。前端拿到的那個 Firebase 登入身份,可以一路傳遞到另一朵雲的後端去做授權。兩朵雲各司其職,一個管「你是誰」,一個管「你的資料」,中間就靠這張 ID token 串起來。
最後快速收個尾。這個專案真正的價值,不在那四個工具各自的功能有多完整,而在它示範的這套方法論。
第一,單一 Vite 建置入口,加上 library-only 的模組設計,讓「共用」這件事變得很自然,不用為了複用而在每個 side project 裡重刻一遍 auth 跟 UI。
第二,按模組挑後端。簡單的存 Firestore,複雜的才上 Workers 加 D1 加 R2,不為了「統一」而過度設計。
第三,跨雲驗證那招——Firebase 發身份、D1 存資料、靠 ID token 串接——是雙雲整合最實用的一個模式。
對一個人的全端實驗場來說,能拿捏好「共用該共用的、隔離該隔離的」這個分寸,其實比一直堆新技術,要難得多了。
Tags
Related Articles
Nutrition Guard: A Zero-Monthly-Cost Multi-Condition Dietary Risk Engine
A pure-TypeScript tag scoring engine computes real-time risk across 140 foods for four conditions — gout, high cholesterol, diabetes, and hypertension. The entire backend runs on Cloudflare's free tier, for a monthly cost of $0.
Live English Tutor: Building a Real-Time Voice AI English Tutor with LiveKit + Gemini Native Audio
A real-time-voice-first AI English tutoring system: students converse with the AI teacher Emma via microphone (optionally with video/screen sharing), the system corrects mistakes in real time, and generates a post-class report in Chinese. The technical core is LiveKit (Self-hosted WebRTC) + Google Gemini 2.5 Flash Native Audio, with a FastAPI backend handling auth, courses, and data persistence.
arXiv Knowledge Assistant: Automated Paper Retrieval and a Bilingual RAG Q&A Platform
A microservice platform orchestrated with Docker Compose: it crawls arXiv papers daily, builds a Qdrant vector index, and delivers bilingual RAG Q&A through hybrid search + re-ranking + Ollama, with email subscriptions and Grafana monitoring.