Guidance for Claude Code when working in this repository.
qtapp — a personal "Quiet Time" (QT) Bible devotional and journaling app, live in production at https://qt.navigators.tech (The Navigators ministry). Built around the PRESS method (Pray, Read, Examine, Say it back, Share). Cut over from a ~3-year-old Nuxt 2 codebase to a full Nuxt 4 rewrite on 2026-07-23 — see docs/migration-plan.md for the full migration history and cutover runbook.
NavigatorsTech/DevoProject— the official org repo. Its GitHub Actions (.github/workflows/deploy.yml) is what actually deploys to production, with the realSSH_HOST/SSH_USER/SSH_KEY/SSH_PORTsecrets. Pushing to itsmastertriggers a real deploy.rogeryeosgit/DevoProject— where day-to-day development happens (this machine'sorigin). Treat it as the working/backup repo; changes need to be separately pushed/PR'd intoNavigatorsTech/DevoProjectto actually reach production. GitHub shows a fork relationship between the two (NavigatorsTech is technically a fork of rogeryeosgit), so a cross-repo compare/PR works viahttps://github.com/NavigatorsTech/DevoProject/compare/master...rogeryeosgit:DevoProject:master.
- Nuxt 4 (
^4.0.0), Vue 3, SSR (ssr: true) — NOT React, NOT Nuxt 2 anymore. - Vuetify 3 (via
vuetify-nuxt-module) for UI (Material Design). Styling is almost entirely Vuetify utility/component classes + theme colors innuxt.config.ts; there is almost no custom CSS. - Pinia for state (
stores/dir, one module per namespace), file-based routing (pages/dir, Nuxt 4's[param]bracket syntax for dynamic routes — not the old_paramunderscore convention). - Backend: Nitro (
server/api/**/*.{get,post,put,delete}.ts, built into Nuxt 4 — no separate Express process anymore), MongoDB Atlas via Mongoose 8, Firebase auth (client SDK +firebase-admin), ESV Bible API for scripture cached via Nitro'sdefineCachedFunction. - Vue 3 Composition API (
<script setup lang="ts">) throughout. TypeScript is used for all server code and stores; components mix TS and plain JS depending on complexity. No class components, no Options API.
npm run dev— Nuxt dev server (nuxt dev), loads.envautomatically (Nitro's built-in env loading, nodotenvpackage needed).npm run build— production build, emits.output/(Nitro) — NOT.nuxt(that was the old Nuxt 2 shape).npm run start— production server (node .output/server/index.mjs).npm run generate— static site generation.npm run typecheck—nuxt typecheck.
There are no tests, no test framework, no linter/prettier config, and no CI test suite (CI exists — see Deployment below — but it only builds and deploys, it doesn't run tests). Do not assume any of these exist. There is a one-off, read-only data validation script (scripts/validate-data.mjs) used during the migration to sanity-check production data against the new schema — not a general test suite. There's also scripts/mobile-overflow-check.js, a paste-into-DevTools-console snippet (not a Node script) for manually checking mobile-viewport overflow/wrap bugs — see its header comment for usage.
pages/— routed views; each.vueis a route. Dynamic routes use bracket params:journalList/[jid]/,plansList/[pid]/.components/— reusable Vue SFCs (Passage.vue,JournalCard.vue,PlanCard.vue,PlanEditor.vue,PassagePicker.vue(book/chapter/verse picker, backed bydata/bible-books.json),QTJournalEditor.vue,StreakCard.vue).layouts/—default.vue(app-bar + nav drawer).error.vuelives at the repo root (Nuxt 4 convention), not underlayouts/.middleware/— route guards:check-auth.ts(rehydrates token from cookie into the Pinia user store),login-check.ts(redirects unauthenticated users). Attached per-page viadefinePageMeta({ middleware: [...] }).stores/— Pinia modules:journal.ts,passage.ts,plan.ts,user.ts. Access viauseJournalStore()etc. (auto-imported composables), notthis.$store.composables/—useAuthFetch.ts($fetchwrapper that attaches the current user's Bearer token, with a 401-retry-once-then-logout backstop).plugins/—firebase.client.ts(initializes Firebase client SDK, syncs ID token refresh + a rolling 3-day idle-cap forced logout into cookies/Pinia — see its file comment for the exact mechanism).server/— Nitro backend:api/— one file per REST endpoint (plans/index.get.ts,qtJournalEntries/index.post.ts, etc.), replacing the old singlerouter.js.plugins/—mongo.ts,firebase-admin.ts(boot-time initialization, Nitro's plugin convention).models/— Mongoose models (Plan.ts,QTEntry.ts,User.ts).utils/—auth.ts(checkUser/requireOwner— token verification + ownership checks),bible-retrieval.ts(ESV API + Nitro cache).
nuxt.config.ts— central config:runtimeConfig(server secrets, must be supplied viaNUXT_-prefixed env vars — see below), Vuetify theme (dark-mode-only), modules list.docs/migration-plan.md— the full Nuxt 2→4 migration history, decisions, and bugs found.docs/dependency-upgrade-plan.md— a separate, not-yet-started plan for bringing dependencies (and Node itself) up to current versions.
- Passage — a Bible passage fetched from the ESV API. Default fallback: "Proverbs [day-of-month]".
- Plan — a reading plan mapping dates → passages;
Plan.passagesis a nested MongooseMapofMap(month → day → reference). New users get"--- Default Nav Plan ---". - QTEntry (journal entry) — title, thoughts, applicationImplication, date, passageReference.
thoughtsandapplicationImplicationare encrypted at rest viamongoose-field-encryption(server/models/QTEntry.ts). - User — email +
planChosen(currently selected plan id).
GET /api/passages/today(optional?planID=),GET /api/passages?passageReference=POST /api/users/verify(verifies a Firebase ID token, provisions a default-planUserdoc on first login),GET|POST /api/users/planChosenGET|POST|PUT|DELETE /api/plans— auth-guarded viacheckUser; PUT/DELETE also enforcerequireOwner(only creator can edit/delete, 403 not 401 for non-owners).GET|POST|PUT|DELETE /api/qtJournalEntries— same auth/ownership pattern.
Client sign-in/register from pages/auth/index.vue → Firebase client SDK directly (email/password or Google) → plugins/firebase.client.ts's onIdTokenChanged listener syncs the token into cookies (jwt, expirationTime, qtAppID, lastActiveAt) and the Pinia user store. composables/useAuthFetch.ts attaches the Bearer token to API calls. Server verifies with admin.auth().verifyIdToken() (server/utils/auth.ts). A rolling 3-day idle cap force-logs-out if too much time passes between token refreshes — see the plugin's file comment.
Required at runtime, all via NUXT_-prefixed env vars (Nitro's runtimeConfig convention — bare names like MONGODB_ACCESS get silently baked in at build time and ignored at runtime, a real bug found during migration): NUXT_MONGODB_ACCESS, NUXT_ESV_API_KEY, NUXT_MONGOOSE_SECRET, NUXT_CACHE_TTL. Also needs fb-service-account.json (Firebase Admin creds) at repo root — git-ignored. .env is loaded automatically by Nitro in dev, no dotenv package needed.
Self-hosted: PM2 process qtapp on the production server, nginx terminating TLS (existing Let's Encrypt cert) and reverse-proxying plain HTTP to 127.0.0.1:3000 — Nitro does not terminate its own TLS (the old Express app did; that's gone). PM2 config lives only on the server as ecosystem.config.cjs (never committed — holds secrets); ecosystem.config.cjs.example in this repo is the template.
Two PM2/interpreter gotchas confirmed the hard way during the real cutover (see docs/migration-plan.md's "Cutover executed" note for the full story) — both already reflected in ecosystem.config.cjs.example:
interpretermust be an absolute path to a Node 20+ binary (e.g. the nvm-installed one). A bare'node'string resolves against the PM2 daemon's own long-running environment, not whatever's onPATHwhen you runpm2 start— silently ran under the system's old Node and crashed.exec_mode: 'fork'withinstances: 1is the confirmed-working combination on the production box;'cluster'/'max'did not honor the absolute interpreter path correctly there. Revisit only after confirming cluster mode respects it.
CI (NavigatorsTech/DevoProject's .github/workflows/deploy.yml, triggered on push to master): builds under Node 20 (via actions/setup-node for validation, nvm on the server for the actual restart), deploys via git reset --hard origin/master + npm ci + npm run build + PM2 restart, health-checks plain http://127.0.0.1:3000/, and rolls back on failure (verifying the rollback itself succeeded, not just attempting it).
- Match existing style: Vue 3 Composition API (
<script setup>), Vuetify 3 components, TypeScript on server/store code. - No automated tests — verify changes by running
npm run devand exercising the flow manually. For anything touching auth/journal/plan flows, prefer testing against a throwaway Firebase test account over the user's own real account. npm cirequirespackage-lock.jsonto be in sync withpackage.json— if it drifts (e.g. from a straynpm installwithout committing the lock file),npm cifails hard both locally and in CI. Regenerate withnpm installand verifynpm cisucceeds clean before committing.