An anime-inspired notes app — Notion's flexibility + Word's document feel + a soft pastel visual
identity. Full original product spec: instructions.md.
Monorepo (npm workspaces): a standalone API and a fully static frontend, deliberately decoupled so the frontend can later be embedded in Tauri (desktop) or a mobile shell with no changes.
apps/api Hono + Drizzle + Neon Postgres — REST API, owns the database
apps/web Next.js Pages Router, output: 'export' — static frontend, RTK Query data layer
npm install # from repo root — installs both workspaces
cp apps/api/.env.example apps/api/.env.local
# edit apps/api/.env.local: DATABASE_URL (Postgres), JWT_ACCESS_SECRET, REFRESH_TOKEN_TTL_DAYS,
# and optionally SUPER_ADMIN_EMAIL — see apps/api/README.md for details
npm run db:migrate # applies apps/api/drizzle/*.sql to DATABASE_URL
npm run dev:api # apps/api → http://localhost:4000
npm run dev:web # apps/web → http://localhost:3000 (separate terminal)apps/web calls the API at NEXT_PUBLIC_API_URL (defaults to http://localhost:4000 if unset).
Full backend reference (every endpoint, request/response shapes, data model, roles, deploying to
Vercel) lives in apps/api/README.md — that file is kept up to date
and is the source of truth for the API contract.
- Hierarchy: Notebook → Page → Sub-page. A Notebook is just a container (no title/icon of
its own — shown in the UI via a preview of its first Page). A Page belongs to one notebook and
is independently favoritable/searchable. A Sub-page (a Page with a
parentPageId) is reachable only by navigating from its parent's content — never independently favorited or searched. - Frontend is a static export (
output: 'export'innext.config.js) — no Server Actions, no API routes, no SSR. Every page fetches client-side via RTK Query after mount. Dynamic per-record pages (/doc,/notebook,/public-doc,/public-notebook) are addressed with a?id=query string rather than a[id]path segment, because a dynamic path segment needsgetStaticPathsat build time and note/notebook ids are unknown user content — a query-string route is one static HTML file that works for any id. - Editor: BlockNote (block-based, slash-command menu).
Internal sub-page links are plain
/doc/?id=<id>hrefs inside the block content; clicks on them are intercepted (components/Editor.tsx) so navigation stays client-side instead of a full reload or a new tab (BlockNote's own link handling opens new tabs by default — the interception has to happen onmousedown, notclick, to beat it). - State: Redux Toolkit + RTK Query (
apps/web/lib/store.ts,apps/web/lib/apiSlice.ts). A second, separatecreateApiinstance (publicApiSlice) handles the unauthenticated/public/*endpoints so guest requests never go through the authenticated reauth-on-401 pipeline. - Design system: Tailwind, Material3-derived color tokens (see
tailwind.config.js—primary,primary-container,surface,on-surface-variant, etc., lifted from a Stitch export) plus custom type-scale tokens (display-lg,h1,h2,body-md,label-md, …). Literata (serif, headings/body) + Inter (sans, UI labels/captions) vianext/font, self-hosted at build time (no external font CDN — matters for the offline Tauri/mobile embed).components/ui/Input.tsx/Button.tsxare the standard form primitives — reach for these instead of one-off input/button classNames. - Sidebar (
components/AppShell.tsx): a floating pill nav, collapsed by default, opened by a pencil-icon toggle (Framer Motion). Treated as visually finalized — don't restyle it without being explicitly asked to.
Custom access-token + refresh-token JWT (no third-party auth provider) — access tokens are
short-lived (15 min) JWTs, refresh tokens are opaque random strings stored hashed and rotated on
every use, with reuse/theft detection. Full detail in apps/api/README.md's "Auth" section.
Four roles:
- Guest — no account. Can view notebooks their owner has explicitly marked public
(
/public-notebook,/public-doc), read-only. - Author (default for every signup) — full control of their own notebooks/pages only.
- Maintainer — moderation only: can view any notebook (including private, for support/moderation) and unpublish someone else's public notebook, nothing else.
- Super Admin — full read/write on any notebook/page, plus
/admin/users(change roles, block/unblock — blocking takes effect immediately, not after token expiry).
The first Super Admin is bootstrapped via the SUPER_ADMIN_EMAIL env var (apps/api/.env.local)
— sign up or log in with that exact email to become super_admin.
- Notebooks list (home page), a notebook's page feed, single page/sub-page view, Favorites (list/grid toggle), Search (live, debounced), Settings, empty-state "Recent Chats" shell.
- Full page editor: BlockNote content, autosave (700ms debounce), emoji/decorative-icon picker, contentEditable title, reading-time + relative "Updated Xm ago" timestamps, sub-page creation, delete-with-confirm.
- Auth: signup/login/logout, silent refresh-on-load, route guarding,
/forgot-password(UI only — no email-sending backend yet, shows an honest "not available yet" message on submit). - RBAC: role management UI at
/admin/users, public/private notebook toggle + shareable link.
- No notebook delete/archive endpoint — pages can be archived (
DELETE /pages/:id), but there's no equivalent for notebooks. Test notebooks from backend verification runs currently can't be cleaned up through the API. - No password reset backend —
/forgot-passwordis a real page but can't actually send an email (no email provider integration). Would need a Marketplace decision (e.g. Resend) before building the real flow. - No email verification, OAuth/social login, or rate limiting on auth — explicitly out of scope so far.
- Recent Chats (
/chats) is an empty-state shell only. Real AI chat (LangGraph + AI SDK streaming) is planned for a later phase — see "Future phases" reasoning below. - Google Stitch MCP was connected (
stitchMCP server, project-scoped) but its tool listing was failing server-side (can't resolve reference #/$defs/ScreenInstance) as of last check — a Stitch-side bug, not something fixable locally. Current visual design was built by hand matching pasted Stitch HTML exports instead of live MCP calls. - Tauri/mobile packaging hasn't been started — the static-export architecture exists specifically to make this possible later, but no Tauri project exists yet.
apps/api is Hono specifically because MCP (@modelcontextprotocol/sdk) and LangGraph.js are
both framework-agnostic — they mount as routes/handlers in any Node backend — and Hono already
runs natively on Vercel Functions with streaming/SSE/WebSocket support. When the "let AI agents
act on Pokodoc on a user's behalf" phase and the real Recent Chats backend get built, they bolt
onto this same apps/api service with no framework migration.
- Verify against the real database, not mocks.
DATABASE_URLinapps/api/.env.localpoints at a live Neon instance — every backend change in this project has been verified with realcurlcalls against it, including security-critical paths (role boundaries, token reuse/theft detection, immediate block effect). Keep doing that. - No Chrome DevTools MCP or Playwright MCP without the user explicitly asking first.
Frontend changes are verified with
next build(static export must succeed) and careful code review instead. - Don't run unnecessary shell commands — the user has asked for this directly; keep
Bashusage purposeful (a build check, a migration, a targeted grep), not exploratory chains. - The sidebar (
components/AppShell.tsx) is finalized — don't restyle it as a side effect of other work. - A
next buildfailure that mentionsCould not find files for X in build-manifest.jsonor "Export encountered errors" is very likely a transient race (usually from a concurrently runningnpm run devtouching.next), not a real code problem —rm -rf .nextand rebuild before assuming something's actually broken. - Don't fabricate data or fake success states. Where a real backend/feature doesn't exist yet (password reset emails, the "Shared" notebooks tab, chat), the UI says so honestly (e.g. "isn't available yet" / "coming soon") rather than pretending it worked.