|
| 1 | +# ServiceStart — Agent Guide |
| 2 | + |
| 3 | +Codex GENERATED. |
| 4 | + |
| 5 | +Multi-tenant nonprofit management platform (volunteers, events, members, media, newsletters). One codebase + one set of infrastructure serves many nonprofits; each is an `organizations` row, and per-deployment customization happens through DB config rows rather than branches or forks. |
| 6 | + |
| 7 | +Read [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) before making design decisions — it explains _why_ the project is shaped this way. [`docs/DB_INFRA.md`](docs/DB_INFRA.md) covers migrations and preview databases. |
| 8 | + |
| 9 | +## Stack |
| 10 | + |
| 11 | +Next.js (App Router) · React 19 · TypeScript · Hono (API + typed RPC client) · Drizzle ORM · PostgreSQL · BetterAuth · Zod · Tailwind v4 · Vitest + Playwright · pnpm · Netlify. |
| 12 | + |
| 13 | +## Layer Map — where to make a change |
| 14 | + |
| 15 | +Requests flow: **`app/` page → `api/` route → `lib/services/` → Drizzle (`lib/schema.ts`) → Postgres.** |
| 16 | + |
| 17 | +| Directory | What lives here | Touch it when… | |
| 18 | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | |
| 19 | +| `app/` | Next.js routes. Pages are **server components** by default; they auth-check, fetch via services, and pass plain data to client components. | Adding/changing a page, route-level access control, or SSR data loading. | |
| 20 | +| `components/` | React components, grouped loosely by feature (`events/`, `navigation/`, `notifications/`, `dashboard/`). Client components are marked `"use client"`. | Any UI work. | |
| 21 | +| `components/bog/` | **Generated** BoG Design System primitives (`BogButton`, `BogTable`, …), vendored via `bog-cli` and tracked in `bog.json`. ESLint-ignored. | Never hand-edit. Add a component with the BoG CLI; if a primitive needs behavior it doesn't have, wrap it in `components/`. | |
| 22 | +| `api/` | Hono sub-apps, one file per resource. Validation (Zod), auth checks, and orchestration only. | Adding/changing an HTTP endpoint. | |
| 23 | +| `lib/services/` | All database access and business logic. Plain async functions bundled into an exported object (`export const XService = { … }`). | Anything involving a query, a write, or logic shared between a page and an API route. | |
| 24 | +| `lib/` (root) | Cross-cutting infra: `schema.ts`, `db.ts`, `auth.ts`, `authUtils.ts`, `app.ts`, `api.ts`, `errors.ts`, `navbar.ts`, `utils.ts`. | Schema changes, auth helpers, nav entries, shared utilities. | |
| 25 | +| `lib/hooks/` | Client-side React hooks (org config, active org, notification polling). | Client state or client-side fetching. | |
| 26 | +| `drizzle/` | **Generated** SQL migrations. | Never hand-write; see Database below. | |
| 27 | +| `tests/unit/` | Vitest (`*.test.ts[x]`), run against a real test Postgres. | Services, API routes, hooks, pure utils. | |
| 28 | +| `tests/e2e/` | Playwright (`*.spec.ts`). | User-visible flows across pages. | |
| 29 | +| `scripts/` | `tsx` scripts for seeding and one-time setup (org, Juno, file storage). | Local/dev setup or seed data. | |
| 30 | +| `juno/` | Git **submodule** — BoG's shared file/email infra. | Never edit; it's a separate repo. | |
| 31 | +| `styles/globals.css` | Tailwind v4 `@theme` — design tokens (colors, type scale, breakpoints). | Adding a color or token, rather than hardcoding a hex value. | |
| 32 | + |
| 33 | +Nothing outside `lib/services/` (and the auth/config plumbing in `lib/`) should import `db` or talk to Drizzle directly. |
| 34 | + |
| 35 | +## Multitenancy — the rule that applies to almost every ticket |
| 36 | + |
| 37 | +Every user-facing entity belongs to an organization. Practically: |
| 38 | + |
| 39 | +- **Scope every query by `organizationId`.** Getting a row by ID alone is a tenant-leak bug; services take the org ID and filter on it. Verify the fetched row's `organizationId` matches the caller's active org before returning it. |
| 40 | +- The active org comes from `session.session.activeOrganizationId`, or is derived from the request's host subdomain (`getSlugFromHost`). Locally, tenants are subdomains of `lvh.me`. |
| 41 | +- **Features are per-org toggles, not per-org code.** Build a feature for everyone, add an `OrganizationConfigKey` for it defaulting to off, and enable it with a config row. Never branch the codebase or special-case a nonprofit by name. |
| 42 | +- **Enforce the toggle on the server too** — both the page and its API routes. Hiding a nav item is not access control. |
| 43 | +- BetterAuth is customized so one email can exist across multiple orgs (see the overrides in `lib/authUtils.ts`). Don't assume email is globally unique. |
| 44 | + |
| 45 | +## Patterns to follow |
| 46 | + |
| 47 | +**API routes.** One Hono sub-app per resource in `api/`, chained method-by-method (`new Hono().get(...).post(...)`) — the chaining is what gives the client its types, so don't split the chain into separate statements. Validate every input with `zValidator("json" | "query" | "param", schema)` and read it back with `c.req.valid(...)`. |
| 48 | + |
| 49 | +**Registering a route requires two edits.** New sub-apps must be added to _both_ `lib/app.ts` (used by the typed client and by tests) and `app/api/[[...route]]/route.ts` (the actual Netlify handler). Forgetting the second one produces a route that passes tests and 404s in the browser. |
| 50 | + |
| 51 | +**Auth in routes.** Prefer the guards in `lib/authUtils.ts` — `requireAuth`, `requireMembership`, `requireAdmin` — which throw typed `HTTPException`s from `lib/errors.ts`. Older routes hand-roll session checks and `c.json({ error }, 401)`; match the helper style in new code and migrate opportunistically. In server pages, use `redirectIfNotMember()` / `redirectIfNotAdmin()`, which redirect rather than throw. |
| 52 | + |
| 53 | +**Client → server calls** go through the typed Hono RPC client: `import api from "@/lib/api"`, then `api.notifications[":id"].$patch({ param: { id }, json: { read } })`. Don't write raw `fetch` calls to `/api/...` — you lose end-to-end types. (`fetch` is fine for file uploads and external services.) |
| 54 | + |
| 55 | +**Server pages** fetch through services directly, not through the RPC client, and serialize non-JSON values (`Date` → ISO string) before passing them into client components. |
| 56 | + |
| 57 | +**Services** are plain functions collected into one exported object at the bottom of the file. Keep them free of HTTP concerns — no `Context`, no status codes; throw or return `null` and let the route decide the response. |
| 58 | + |
| 59 | +**Schema.** TypeScript enums in `lib/schema.ts` are the source of truth; `pgEnum` and Zod schemas are derived from them, so add a value in one place. Reuse those enums in validation instead of restating string literals. |
| 60 | + |
| 61 | +**Styling.** Tailwind utility classes with the semantic tokens from `styles/globals.css` (`text-brand-text`, `bg-grey-fill-weak`, …) — avoid raw hex and arbitrary values. `components/bog/` primitives use CSS modules; that's their convention, not the app's. |
| 62 | + |
| 63 | +**Path aliases.** Import with `@/` (e.g. `@/lib/services/EventService`), not long relative chains. |
| 64 | + |
| 65 | +## Database |
| 66 | + |
| 67 | +Schema lives in `lib/schema.ts`; migrations in `drizzle/` are generated, never written by hand. |
| 68 | + |
| 69 | +```bash |
| 70 | +pnpm run db:generate # after editing lib/schema.ts |
| 71 | +pnpm run db:migrate # apply to local dev DB |
| 72 | +pnpm run db:check # verify migration consistency (CI runs this) |
| 73 | +pnpm run db:view # Drizzle Studio |
| 74 | +pnpm run db:seed |
| 75 | +``` |
| 76 | + |
| 77 | +CI also auto-generates and commits migrations on push. If a generated migration looks wrong after a merge, delete and regenerate the PR's migration files rather than editing them. |
| 78 | + |
| 79 | +## Commands |
| 80 | + |
| 81 | +```bash |
| 82 | +pnpm dev # Juno + local DB + Next dev server |
| 83 | +pnpm test # unit tests (starts/stops the test DB on :5433) |
| 84 | +pnpm test:e2e # Playwright |
| 85 | +pnpm lint # zero warnings allowed |
| 86 | +pnpm format:fix # Prettier (a pre-commit hook checks formatting) |
| 87 | +``` |
| 88 | + |
| 89 | +Docker must be running: the dev and test databases and Juno are all containers. |
| 90 | + |
| 91 | +## Testing expectations |
| 92 | + |
| 93 | +- Unit tests hit a **real** Postgres, not mocks. Build fixtures with the helpers in `tests/unit/testUtils.ts` (`createOrganization`, `buildTestUser`, `signUpAndGetSession`, `addMember`, …) and call API routes through `testApi` (Hono's `testClient`). |
| 94 | +- Add `// @vitest-environment node` at the top of server-side test files; the default environment is jsdom for component tests. |
| 95 | +- New API routes and services get unit tests; new user-facing flows get an e2e test. Cover the multi-tenant case explicitly — a test that data from org A is invisible to org B is the one that catches the bug that matters here. |
| 96 | +- CI runs build, lint, format, unit tests, sharded e2e, and migration checks. All must pass. |
| 97 | + |
| 98 | +## Gotchas |
| 99 | + |
| 100 | +- `lib/app.ts` and `app/api/[[...route]]/route.ts` duplicate the route registration list — keep them in sync. |
| 101 | +- `juno/` is a submodule; a fresh clone needs `git submodule update --init --recursive` plus `pnpm run juno:setup`. |
| 102 | +- `DB_URL` contains a `<branch>` placeholder that `getDbUrl()` in `lib/db.ts` substitutes so PR preview deploys hit their own preview database. Don't remove it from `.env`. |
| 103 | +- File storage is pluggable via `FILE_SERVICE_IMPLEMENTATION` (`local` | `juno`) behind the `IFileService` interface — code against the interface, not a concrete implementation. Juno deletion is not implemented yet. |
| 104 | +- ESLint ignores `components/bog/` and `juno/`; don't "fix" lint errors there. |
0 commit comments