ZomLab is a personal software engineering lab and executable knowledge base. It keeps working examples, their documentation, and the infrastructure needed to run them in one TypeScript monorepo.
The completed Core curriculum includes routing, forms, validation, state management, data fetching, authenticated notes CRUD, server-side search/sorting/pagination, tables, R2 file uploads, error handling, caching, middleware, and logging. Every implemented topic pairs MDX documentation with a working demo and browser coverage; navigation outside Core remains planned work.
| Area | Implementation |
|---|---|
| Runtime | Node.js 24.18.1 or newer, pnpm 11.24.0 |
| Monorepo | pnpm workspaces and Turborepo 2 |
| Web application | React 19, TanStack Start, TanStack Router, Vite 8 |
| Client data | TanStack Query and the typed Hono client |
| API | Hono with OpenAPI route definitions and Zod 4 contracts |
| Authentication | Better Auth with email and password, optional GitHub and Google OAuth, and a development-only magic-link plugin |
| Database | PostgreSQL on Neon, Drizzle ORM, and Drizzle Kit migrations |
| Styling and content | Tailwind CSS 4, MDX, GitHub Flavored Markdown, Shiki, and React Flow |
| Deployment | Cloudflare Workers through the Cloudflare Vite plugin and Wrangler |
| Tests and component docs | Vitest 4, Playwright, and Storybook 10 |
| Quality tooling | Biome, zizmor, syncpack, Knip, TypeScript, Husky, lint-staged, and Changesets |
Dependency ranges live in the workspace manifests. Resolved versions live in pnpm-lock.yaml.
Install these tools and services before running the app:
- Node.js 24.18.1 or newer
- pnpm 11.24.0
- A PostgreSQL database reachable through a URL accepted by the Neon serverless driver
- A 32-character or longer Better Auth secret
Cloudflare credentials are only required for deployment and Worker validation commands that contact Cloudflare.
Install the locked dependencies:
pnpm install --frozen-lockfileCreate the local environment file:
cp .env.example .envEdit .env with a working database URL, authentication secret, and local authentication URL. Apply the checked-in migration, then start the app:
pnpm db:migrate
pnpm devThe development server uses port 3000 unless E2E_PORT overrides it. pnpm dev clears listeners on ports 3000 through 3005 and 8787 before starting the @zomlab/web Turbo task.
For a disposable development database, pnpm db:push synchronizes the schema directly instead of applying migration history.
The optional pnpm setup helper runs pnpm install, copies .env.example when .env is missing, and runs pnpm db:generate. It does not apply or push the database schema.
Server variables are validated lazily by @zomlab/env. Invalid required values throw an error when server configuration is first accessed.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection URL used by Drizzle and Neon |
BETTER_AUTH_SECRET |
Yes | Better Auth signing secret, minimum 32 characters |
BETTER_AUTH_URL |
Yes | Fallback base URL for authentication requests |
BETTER_AUTH_ALLOWED_HOSTS |
Recommended | Comma-separated hostnames accepted by Better Auth; the host from BETTER_AUTH_URL is always added |
APP_ENV |
No | staging or production; defaults to staging |
E2E_PORT |
No | Development and Playwright server port; defaults to 3100 in environment validation and Playwright, while Vite defaults to 3000 when it is unset |
BETTER_AUTH_GITHUB_CLIENT_ID |
No | Enables GitHub OAuth when paired with BETTER_AUTH_GITHUB_CLIENT_SECRET |
BETTER_AUTH_GITHUB_CLIENT_SECRET |
No | GitHub OAuth secret |
BETTER_AUTH_GOOGLE_CLIENT_ID |
No | Enables Google OAuth when paired with BETTER_AUTH_GOOGLE_CLIENT_SECRET |
BETTER_AUTH_GOOGLE_CLIENT_SECRET |
No | Google OAuth secret |
The deployed Worker also requires the MY_RATE_LIMITER Cloudflare rate-limit binding and the FILE_UPLOADS R2 binding. apps/web/wrangler.jsonc maps staging to zomlab-uploads-staging, production to zomlab-uploads, and places Worker backend execution near the Singapore Neon database with aws:ap-southeast-1.
Local development uses Cloudflare's local R2 simulation. Uploaded objects remain under apps/web/.wrangler/state/v3/r2 and do not appear in the Cloudflare dashboard unless a binding is intentionally configured for remote development.
Keep .env and apps/web/.dev.vars private. The repository tracks only .env.example.
The browser renders TanStack Router routes from apps/web. Requests under /api/* enter TanStack Start server handlers and are forwarded to the Hono application. Auth requests use the Better Auth handler directly.
Browser
-> TanStack Start routes and React components
-> TanStack Query hooks
-> typed Hono client
-> /api/* TanStack Start server route
-> Hono middleware and OpenAPI routes
-> NoteService
-> NoteRepository
-> Drizzle ORM
-> Neon PostgreSQL
/api/auth/*
-> Better Auth
-> Drizzle adapter
-> Neon PostgreSQL
The API currently exposes:
GET /api/healthGET /api/readyGET /api/versionGET /api/notesandGET /api/notes/:idPOST /api/notesPATCH /api/notes/:idDELETE /api/notes/:idGET /api/filesandGET /api/files/:idPOST /api/filesDELETE /api/files/:id- Better Auth endpoints under
/api/auth/* - An OpenAPI 3.1 document for notes at
/api/notes/docs - An OpenAPI 3.1 document for files at
/api/files/docs
All notes and file routes require a session and scope storage operations to the authenticated user. Private HTML, authentication, notes, and file responses disable shared caching.
The notes route creates its repository and injects it into createNoteService(repository) at the composition boundary. The service returns null only when a read, update, or delete cannot find an owned note. Routes translate that result to a 404 response. Repository failures continue to the shared Hono error handler, which logs the private failure and returns a masked 500 response.
Client hooks parse unsuccessful responses through readJsonResponse. Valid public error envelopes retain their message, status, code, and optional detail in ApiResponseError; malformed responses use stable fallback copy. Shared queryKeys factories keep health, file, note-list, and note-detail cache identities consistent across queries and invalidation. Note and file deletion require an accessible confirmation dialog before the mutation runs.
apps/
web/ TanStack Start app, Hono API, UI, routes, and lab content
packages/
auth/ Better Auth server options and React client
contracts/ Shared Zod request and response schemas
database/ Drizzle client, schema, migrations, and repositories
env/ Runtime environment validation
tsconfig/ Shared TypeScript configurations
ui/ Small Storybook component workspace
vitest-config/ Shared Vitest defaults
scripts/ Setup, package generation, version sync, and workflow linting
e2e/ Playwright browser and API contract tests
.github/workflows/ Continuous integration, end-to-end tests, and deployment
graphify-out/ Generated code knowledge graph
Inside apps/web/src:
routes/contains TanStack Router file routes and API server routesintegration/hono/contains Hono composition, middleware, errors, feature routes, and serviceslabs/core/contains the Core overview content, interactive demos, and TanStack Query hookscomponents/contains shared application componentslib/contains the typed API client, auth server function, navigation model, and shared helpersstyles/contains Tailwind theme tokens and global styles
Run root commands from the repository root.
| Command | Effect |
|---|---|
pnpm setup |
Install dependencies, create .env when missing, and generate Drizzle migrations |
pnpm reset |
Clean generated local state, reinstall dependencies, and generate migrations |
pnpm dev |
Clear development ports and start the staging Worker-compatible app |
pnpm dev:types |
Watch workspace type-check tasks |
pnpm dev:clean |
Clear Turbo state |
pnpm dev:debug |
Start development with the Node inspector on port 9229 |
pnpm kill:ports |
Stop listeners on development ports |
pnpm kill:e2e-port |
Stop the listener on E2E_PORT or port 3100 |
pnpm clean |
Remove workspace dependencies, caches, builds, reports, and generated output |
| Command | Effect |
|---|---|
pnpm build |
Build the staging app and type-check it |
pnpm build:production |
Build the production app and type-check it |
pnpm deploy |
Build and deploy the staging Worker |
pnpm deploy:production |
Build and deploy the production Worker |
pnpm versions:upload |
Upload a staging Worker version without deploying it |
pnpm versions:upload:production |
Upload a production Worker version without deploying it |
pnpm cf:typegen |
Regenerate Cloudflare Worker types |
pnpm cf:validate |
Check Worker types and dry-run both deployment environments |
pnpm cf:startup |
Build the app and capture a Worker startup profile |
| Command | Effect |
|---|---|
pnpm check:all |
Run workflow linting, Biome, syncpack, Knip, TypeScript, Node Vitest, and workerd Vitest |
pnpm lint:workflows |
Run zizmor against all GitHub Actions workflows (install with uv tool install zizmor==1.29.0) |
pnpm lint |
Check repository files with Biome |
pnpm lint:fix |
Apply Biome fixes |
pnpm format |
Format repository files with Biome |
pnpm format:check |
Check formatting without changes |
pnpm check-types |
Run all workspace type checks through Turbo |
pnpm check-types:watch |
Watch all workspace type checks |
pnpm test |
Run the Vitest workspace with environment loading |
pnpm test:workers |
Run Worker-only Vitest tests in workerd with local Cloudflare bindings |
pnpm test:turbo |
Run package test tasks through Turbo |
pnpm test:watch |
Start Vitest in watch mode |
pnpm test:e2e |
Start the app and run Playwright tests |
pnpm test:staging-smoke |
Verify the deployed staging health/version contracts and request IDs |
pnpm deps:check |
Check dependency policies with syncpack |
pnpm deps:fix |
Apply syncpack dependency fixes |
pnpm deps:unused |
Check unused files, exports, and dependencies with Knip |
pnpm security:audit |
Audit production dependencies |
pnpm security:check |
Audit all dependencies |
pnpm storybook |
Start the @zomlab/ui Storybook on port 6006 |
pnpm build-storybook |
Build the static Storybook |
The staging Worker is protected by Cloudflare Access. For automated smoke checks, provision an
Access service token with a matching Service Auth policy and provide CF_ACCESS_CLIENT_ID and
CF_ACCESS_CLIENT_SECRET to the command. Do not commit either value.
| Command | Effect |
|---|---|
pnpm db:generate |
Generate Drizzle migrations from the schema |
pnpm db:migrate |
Apply checked-in migrations |
pnpm db:push |
Push the current schema directly to the database |
pnpm db:pull |
Introspect the database schema |
pnpm db:export |
Export the Drizzle schema as SQL |
pnpm db:check |
Validate migration consistency |
pnpm db:up |
Upgrade Drizzle snapshot metadata |
pnpm db:studio |
Start Drizzle Studio |
pnpm generate:package <name> |
Create a kebab-case package scaffold under packages/ |
pnpm changeset |
Create a Changesets release note |
pnpm version:packages |
Apply pending changesets to workspace versions |
pnpm version:sync |
Copy the root version to app and package manifests |
pnpm with-env -- <command> is the internal wrapper used by scripts that need values from .env. The prepare lifecycle installs Husky hooks.
pnpm test discovers Node and browser-like Vitest projects from the app, packages, and scripts/. pnpm test:workers runs *.worker.test.ts files separately in workerd with the staging Wrangler configuration and local bindings. Together they cover API error normalization, service boundaries, query keys, storage behavior, redirect safety, authentication environment hardening, environment precedence, workflow syntax, package-manager policy, setup behavior, and package generation.
Playwright starts @zomlab/web on E2E_PORT, then exercises:
- Authentication redirects, session persistence, logout, and relogin
- Notes creation, editing, deletion, ownership, validation, and API contracts
- System API contracts
- MDX, syntax highlighting, and interactive React Flow diagrams
- Page structure, cache headers, theme behavior, and visual captures
The end-to-end suite creates real users and notes in the configured database. Use an isolated development or staging database.
apps/web/wrangler.jsonc defines 2 Workers:
zomlab-stagingfor the Wranglerstagingenvironmentzomlabfor the Wranglerproductionenvironment
Both environments use the nodejs_compat flag, observability, preview URLs, and the MY_RATE_LIMITER binding. Store database and authentication secrets in Cloudflare. Do not add them to wrangler.jsonc.
GitHub Actions deploys the dev branch to staging and main to production. The deployment workflow validates and applies Drizzle migrations before it deploys the Worker. CI also builds the app, regenerates Worker types, performs deployment dry runs, and audits production dependencies.