This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Abide Connect is a Nuxt 4 PWA for Abide Women's Health Services: volunteer/donor engagement, event management, mobile clinic locator, and an admin dashboard. Package manager is pnpm (required — .npmrc/pnpm-workspace.yaml assume it; don't use npm/yarn).
pnpm install # install deps (runs `nuxt prepare` via postinstall)
pnpm dev # dev server at http://localhost:3000
pnpm build # production build
pnpm generate # static generation
pnpm preview # preview a production build
pnpm lint # eslint .
pnpm lint:fix # eslint . --fix
pnpm db:generate # prisma generate (regenerate client after schema changes)There is no test suite configured in this repo currently.
Schema lives across multiple files under prisma/schema/ (not a single schema.prisma), configured via prisma.config.ts. The generated client is emitted to server/utils/generated/prisma (not node_modules), imported in server/utils/prisma.ts via a PrismaClient + @prisma/adapter-better-sqlite3 singleton.
pnpm prisma generate # regenerate client after editing prisma/schema/*.prisma
pnpm prisma migrate dev # create/apply a migration
pnpm prisma db seed # seed from prisma/seed/*.json via server/utils/seed.ts
pnpm prisma migrate reset # reset DB, then re-seedAfter changing any file in prisma/schema/, always run pnpm prisma generate (and a migration if the change is structural) — the app imports types directly from the generated output.
Copy .env.example to .env before running anything. Required vars: BETTER_AUTH_SECRET, BETTER_AUTH_URL, DATABASE_URL, IMAGE_STORAGE_PATH/IMAGE_URL_PATH (local image storage), EMAIL_HOST/EMAIL_PORT/EMAIL_USER/EMAIL_PASS/EMAIL_FROM (SMTP via nodemailer, used for OTP emails). Google OAuth uses OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET (see server/utils/auth.ts). GOOGLE_CALENDAR_ID is the shared calendar events are synced to (see below).
When an event is created/updated/deleted, server/utils/googleCalendar.ts mirrors it to a single shared Google Calendar (GOOGLE_CALENDAR_ID). Writes use the acting volunteer's Google OAuth token — obtained via auth.api.getAccessToken({ body: { providerId: 'google', userId } }), which refreshes using the stored refresh token — so the Google provider requests the calendar.events scope with accessType: 'offline' + prompt: 'consent' (in auth.ts). The Google event id is stored on Event.calendarEventId (and its link on Event.calendarURL) for later update/delete. Sync is best-effort: if the acting volunteer signed in with email OTP (no Google token) or the Calendar API fails, the DB operation still succeeds and the calendar is simply not updated.
This is a Nuxt 4 app, so the app source root is app/ (pages, components, layouts, middleware, lib, types), while server/ holds Nitro server routes/middleware/utils and is a separate root — cross-referencing it from app/ code goes through the #server path alias (e.g. import { auth } from '#server/utils/auth'), not a relative path.
server/utils/auth.tsconfiguresbetter-authwith the Prisma adapter, pointed at a customVolunteermodel (viauser: { modelName: 'Volunteer', fields: { image: 'imageURL' } }) instead of the defaultUsermodel — the schema's ownUsermodel (prisma/schema/user.prisma) is a separate concept representing RSVP guests/contacts, not the authenticated principal.- Two auth methods are wired: Google OAuth (
socialProviders.google) and email OTP (emailOTPplugin,disableSignUp: true— sign-up must happen through the app's own sign-up flow, not automatically on first OTP request). OTP emails are sent via a nodemailer SMTP transport. server/api/auth/[...all].tsmounts the better-auth handler for all/api/auth/*routes.server/middleware/authenticated.tsruns on every request, attachesevent.context.session, and hard-redirects unauthenticated requests to/auth/loginfor any path under/volunteeror/api/volunteer.app/middleware/auth.tsis the client-side route guard: it calls/api/auth/get-sessionand redirects to/auth/loginunless the route is in itspublicRoutesallowlist. Add new public pages to that list explicitly.server/utils/auth-client.tsexports thebetter-auth/vueclient (authClient) for use in components/pages.
Split by domain: user.prisma (RSVP-only guest User), volunteer.prisma (the authenticated Volunteer plus availability/certification/hour-log/language join tables), event.prisma (Event, Event_Asset, RSVP, GuestRSVP), donation.prisma, location.prisma, mobileClinic.prisma, notification.prisma. schema.prisma itself only defines the generator/datasource plus better-auth's own Session/Account/Verification models — those FK to Volunteer, not User, per the auth config above.
Note the two distinct "attendee" concepts: User (guest, RSVP-capable, no login) vs Volunteer (authenticated, can log hours/certifications). A Volunteer can optionally link to a User record (Volunteer.userId).
File-based Nitro routes, one file per HTTP method (e.g. events/index.get.ts + events/index.post.ts, events/[id]/index.patch.ts). Admin-only routes live under server/api/admin/; there's no separate role-middleware yet, so authorization checks happen inline per-handler where present — check event.context.session for the current principal. Server code imports Prisma via #server/utils/prisma (default export).
pages/mirrors routes directly (Nuxt file-based routing):auth/,admin/,events/,volunteer/, plus top-level pages likesettings.vue,inbox.vue,mobileClinic.vue.components/grouped by feature (event/,map/,nav/), not by type.map/Interactive.client.vue's.client.vuesuffix means it's client-only (maplibre-gl doesn't SSR) — follow this pattern for any other browser-only integrations.lib/relativeFetch.tsstrips the origin off absolute URLs before calling Nuxt'suseFetch, to avoid SSR/CSR host mismatches — prefer it over rawuseFetch/$fetchwhen a URL might be absolute.- UI kit is
@nuxt/ui(v4) with Tailwind v4; custom theme colors (brand1–brand8plus standard semantic colors) are declared innuxt.config.tsunderui.theme.colorsand expected to be defined inapp/assets/css/main.css. - PWA config (icons, manifest, workbox) lives in
nuxt.config.tsunder thepwakey —navigateFallbackAllowlistdeliberately excludes/apiso API calls aren't intercepted by the service worker.
main and stage branches each have a GitHub Actions workflow (.github/workflows/main.yml / stage.yml) that builds a Docker image (arm64), pushes to ECR, and force-redeploys the corresponding ECS service — main → prod, stage → stage. There's no CI test/lint gate in these workflows currently, so run pnpm lint and pnpm build locally before merging.