Imported from Irfan140/Stubady (
AGENTS.md). Install upstream withnpx skills add Irfan140/Stubady. Copyright stays with the author.
Stubady — Agent Guide
Independent packages: server (Bun + Express + Prisma + pgvector/Redis/BullMQ/LangChain), mobile (Expo 55 + React Native + Expo Router + Clerk, Node + npm), and web (Vite + React + Tailwind marketing site, Bun + oxlint). No workspaces — each folder is standalone with its own package manager.
Project Structure — Independent Packages (No Bun Workspaces)
Ai-Study-Buddy/
├── server/ # Bun API — src/index.ts, Prisma, BullMQ workers (independent package)
│ ├── eslint.config.js / .prettierrc / .prettierignore # local lint/format
│ └── package.json / bun.lock # run `bun install` inside server/
├── mobile/ # Expo app — src/app/ (Expo Router), src/features/, src/components/ (independent package)
│ ├── eslint.config.js / .prettierrc / .prettierignore # local lint/format
│ └── package.json / package-lock.json # run `npm install` inside mobile/
├── web/ # Marketing site — Vite + React + Tailwind SPA (independent package)
│ ├── .oxlintrc.json # lint config (oxlint, not eslint; no prettier)
│ └── package.json / bun.lock # run `bun install` inside web/
└── docker-compose.yml # postgres (pgvector:pg18) + redis:7 (dev: DB only)
server/, mobile/, and web/ are completely independent — no workspaces field, each has its own lint/format config, lockfile (server/bun.lock, mobile/package-lock.json, web/bun.lock) and node_modules. Run bun install inside server/, npm install inside mobile/, and bun install inside web/ separately. No root package.json — each package is installed independently.
Commands — per package (Bun for server, npm for mobile)
bun install --cwd server # install server only
npm install --prefix mobile # install mobile only (mobile uses npm, not bun)
bun install --cwd web # install web only (web uses bun, not npm)
# alternative: `cd server && bun install` / `cd mobile && npm install` / `cd web && bun install`
bun run --cwd server lint # eslint . inside server/ (uses server/eslint.config.js)
bun run --cwd server lint:fix
bun run --cwd server format # prettier inside server/
npm run --prefix mobile lint # eslint . inside mobile/ (uses mobile/eslint.config.js)
npm run --prefix mobile lint:fix
npm run --prefix mobile format
bun run --cwd web lint # oxlint inside web/ (uses web/.oxlintrc.json; no prettier)
bun run --cwd web build # tsc -b + vite build (also typechecks web)
bun run --cwd server dev
bun run --cwd server dev:worker # ingestion worker (separate process; run alongside dev)
npm run --prefix mobile start
bun run --cwd web dev
bun --cwd=server x prisma migrate dev # create migration (or `cd server && bunx prisma migrate dev`)
bun --cwd=server x prisma generate # regenerate client (also postinstall)
# Mobile / Expo — ALWAYS use expo install for SDK-compatible versions
npx --prefix mobile expo install <pkg> # or `cd mobile && npx expo install <pkg>`
npm run --prefix mobile start # or `cd mobile && npx expo start`
npx --prefix mobile expo-doctor # or `cd mobile && npx expo-doctor`
npx --prefix mobile eas build --profile development --platform android # or eas build
Run bun run --cwd server lint + bun run --cwd server format:check, npm run --prefix mobile lint, and bun run --cwd web lint (plus bun run --cwd web build if TS changed) before declaring any task done.
Server conventions
- Runtime:
Bun+Express 5, API entryserver/src/index.ts(re-exportsserver/src/app.tsfor tests), worker entryserver/src/worker.ts(BullMQ ingestion, separate process). - DB: Prisma 7 with
pgvector(vector(1536)fortext-embedding-3-small), datasourcepostgresql+extensions=[vector]. Config inserver/prisma7.config.ts, schemaserver/prisma/schema.prisma. - Infra:
ioredis+BullMQ(ingestion queue),pino+pino-http(redacted),helmet/compression/express-rate-limit+rate-limit-redis. - Auth:
@clerk/expressverifyTokenviarequireAuthmiddleware;x-access-tokenfallback supported. - AI:
LangChain+LangGraph+OpenAI(chat + embeddings),Firecrawlfor web sources, R2 (S3) for PDFs. - Structure:
src/config/,src/constants/,src/lib/,src/middlewares/,src/routes/,src/controllers/,src/services/,src/repositories/,src/schemas/,src/queues/,src/workers/,src/processors/,src/utils/. - Env: validated with
zodinsrc/config/env.ts(loads.envthen.env.development). Never hardcode secrets — useenv.*. - No
console.log— usepinologger. Keep comments minimal — explain why, not what.
Mobile conventions — Expo has changed, do not trust training data
Before writing any Expo/EAS/React Native code:
- Read
expomajor version frommobile/package.json(currently~55.0.31). - Fetch matching docs:
https://docs.expo.dev/versions/v<major>.0.0/ - For anything else, fetch
https://docs.expo.dev/llms.txtand follow its links. Never answer from memory.
- Navigation: Expo Router only. Routes in
mobile/src/app/— every file is a screen,_layout.tsxdefines navigators. Keep components/hooks/utils outsidesrc/app/. ImportLink,router,useLocalSearchParamsfromexpo-router. ios/andandroid/are CNG (Continuous Native Generation) — never create/edit by hand; configure viamobile/app.config.tsand config plugins.- Expo Go only has bundled natives — after adding native code, build dev client:
npx --prefix mobile expo run:android|iosoreas build --profile development. - Prefer Expo modules over third-party libs. Check
https://docs.expo.dev/versions/latest/index.mdbefore adding deps. - State:
zustand+@tanstack/react-query+zod+react-hook-form. - Path alias
@/*→mobile/src/*(tsconfig.json). experiments.reactCompileris on inmobile/app.config.ts— components are auto-memoized; fix render churn at the source (subscriptions, data identity) instead of hand-memoizing.- Pushed screens hide the native header (
headerShown: false) and render a custom JS back bar (router.back()+SymbolViewchevron) — see(tabs)/index.tsx,study-set/[id].tsx,study-set/[id]/{summaries,decks,conversations}.tsx,chat/[id].tsx. - The tab bar is floating/absolute (
(tabs)/_layout.tsx) — every tab screen must clear it with safe-area-aware bottom padding (listpaddingBottom, FABbottom), or content/buttons end up underneath it. - OTA-safe by default: prefer JS-only changes (styles, JSX, existing deps) so updates ship via EAS Update without a rebuild. New native modules, config plugins, or
app.config.tschanges require a new build — flag this before doing it.
Building with EAS
npx --prefix mobile eas build --profile development --platform android
npx --prefix mobile eas build --profile preview --platform android
npx eas-cli submit / eas update # cloud sign/submit/OTA
Profiles in mobile/eas.json (development/preview/production). Secrets injected via EXPO_PUBLIC_* in .github/workflows/android-build.yml.
Env & secrets
- Never commit
.env/.env.development. Examples inserver/.env.example+mobile/.env.example. docker-compose.ymluses local dev credsmyuser/mypassword— do not reuse in production.- Mobile
EXPO_PUBLIC_*vars are inlined at build time viamobile/src/config/env.ts.
Rules
- Each package owns its lint/format —
server/eslint.config.js/server/.prettierrcandmobile/eslint.config.js/mobile/.prettierrcare independent (no root delegation). - Do not use
yarn/pnpm add— usenpx --prefix mobile expo install(mobile) orbun add(server) and verify SDK compatibility. - Keep lockfiles per package (
server/bun.lock,mobile/package-lock.json); do not delete. - For Prisma changes: edit
server/prisma/schema.prisma, thenbun --cwd=server x prisma migrate devand verifyprisma generate. - For new routes/screens: follow existing
repositories → services → controllers → routes → schemas(server) andsrc/features/*/api.ts+src/app/(mobile) patterns. - For web: Bun + Vite SPA (
web/), lint withoxlint(bun run --cwd web lint), typecheck viabun run --cwd web build. Hash routes live inweb/src/router.ts(home/privacy/delete-account); keep marketing claims inweb/src/pages/Home.tsxconsistent with shipped app features.