Skip to content

Commit f2a0407

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/prettier-3.8.3
2 parents 69d10f4 + a25148a commit f2a0407

124 files changed

Lines changed: 13637 additions & 2894 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.template‎

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,50 @@
1+
# Required: application PostgreSQL connection. Local db:create credentials below.
12
DB_URL=postgres://dev:root@localhost:5432/postgres
2-
3+
# Required: generate a private value with openssl rand -base64 32.
34
BETTER_AUTH_SECRET=
5+
# Optional: authentication origin; defaults to the application's base URL.
46
BETTER_AUTH_URL=http://localhost:3000
5-
6-
NEXT_PUBLIC_BASE_URL= # Optional. Defaults to http://localhost:3000
7-
8-
FILE_STORAGE_DIR=/storage
9-
FILE_SERVICE_IMPLEMENTATION=local # "local" (default) or "juno"
10-
7+
# Optional: public application origin; defaults to http://localhost:3000.
8+
NEXT_PUBLIC_BASE_URL=
9+
# Optional: preview database branch substitution; defaults to main.
10+
NEXT_PUBLIC_BRANCH_NAME=
11+
# Optional: production URL used for main-- deployment URLs.
12+
NEXT_PUBLIC_PROD_URL=
13+
# Optional: true or 1 disables client organization-configuration caching.
14+
NEXT_PUBLIC_ORG_CONFIG_CACHE_DISABLED=
15+
16+
# Optional: local (default) or juno file storage.
17+
FILE_SERVICE_IMPLEMENTATION=local
18+
# Required for local file storage: directory relative to the repository root.
19+
FILE_STORAGE_DIR=./storage
20+
21+
# Required for Juno operations: generated by pnpm run juno:setup.
1122
JUNO_API_KEY=
23+
# Required for local Juno setup; optional at runtime (SDK defaults to hosted Juno).
1224
JUNO_BASE_URL=http://localhost:8888
13-
14-
FILE_PROVIDER_NAME=servicestart-dev
15-
25+
# Required for email: base sender domain, without mail. or a URL scheme.
26+
# This .test domain is ONLY for default local simulation. Use a verified domain for real delivery.
27+
EMAIL_SENDER_DOMAIN=notifications.test
28+
# Required for email setup only. The SDK requires an SG prefix even in simulation.
29+
# Fake key for local simulation; obtain an approved key for real delivery.
30+
SENDGRID_KEY=SG.test-sendgrid-key
31+
32+
# Required for Juno file storage: project ID from the Juno administrator.
1633
JUNO_PROJECT_ID=
34+
# Required for Juno file storage/provider setup: registered provider name.
35+
FILE_PROVIDER_NAME=servicestart-dev
36+
# Optional: Juno bucket name prefix; defaults to ServiceStart.
1737
JUNO_FILE_BUCKET_PREFIX=ServiceStart
18-
38+
# Required for Azure provider setup and Juno file URL resolution.
1939
AZURE_STORAGE_ACCOUNT_NAME=
40+
# Required only for Azure provider setup: obtain securely from the administrator.
2041
AZURE_STORAGE_ACCOUNT_KEY=
21-
42+
# Required only to register an organization's file bucket through setupOrg.
2243
SETUP_ORGANIZATION_ID=
23-
SENDGRID_KEY=
44+
45+
# Optional: comma-separated additional Next.js development origins.
46+
ALLOWED_DEV_ORIGINS=
47+
# Optional: Playwright target origin; defaults to http://localhost:3000.
48+
BASE_URL=
49+
# Optional: tenant domain override for tests; .env.test uses lvh.me.
50+
E2E_TENANT_DOMAIN=

‎.env.test‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ FILE_SERVICE_IMPLEMENTATION=local
1212
JUNO_API_KEY=test-juno-api-key
1313
JUNO_BASE_URL=http://localhost:8888
1414
SENDGRID_KEY=SG.test-sendgrid-key
15+
EMAIL_SENDER_DOMAIN=notifications.test
1516

1617
ALLOWED_DEV_ORIGINS=a.lvh.me,b.lvh.me
1718
E2E_TENANT_DOMAIN=lvh.me

‎.github/workflows/ci.yml‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ jobs:
2323

2424
- name: Build
2525
run: pnpm build
26+
env:
27+
DB_URL: postgres://test:root@localhost:5433/postgres
28+
BETTER_AUTH_SECRET: ci-build-only-secret-not-for-deployment
2629

2730
test:
2831
runs-on: ubuntu-latest
@@ -43,6 +46,22 @@ jobs:
4346
e2e:
4447
name: E2E Test - shard ${{ matrix.shard }}/${{ matrix.shardTotal }}
4548
runs-on: ubuntu-latest
49+
services:
50+
postgres:
51+
image: postgres
52+
env:
53+
POSTGRES_USER: test
54+
POSTGRES_PASSWORD: root
55+
POSTGRES_DB: postgres
56+
ports:
57+
- 5433:5432
58+
# Check TCP: the temporary initialization server only accepts sockets.
59+
# Actions waits for health before running any migration or test steps.
60+
options: >-
61+
--health-cmd "pg_isready -h 127.0.0.1 -p 5432 -U test -d postgres"
62+
--health-interval 2s
63+
--health-timeout 5s
64+
--health-retries 30
4665
strategy:
4766
fail-fast: false
4867
matrix:
@@ -59,9 +78,6 @@ jobs:
5978
- name: Install dependencies
6079
run: pnpm install --frozen-lockfile
6180

62-
- name: Create test database
63-
run: pnpm run db:test:create
64-
6581
- name: Migrate test database
6682
run: pnpm run db:test:migrate
6783

@@ -112,6 +128,21 @@ jobs:
112128

113129
check-migrations:
114130
runs-on: ubuntu-latest
131+
services:
132+
postgres:
133+
image: postgres
134+
env:
135+
POSTGRES_USER: dev
136+
POSTGRES_PASSWORD: root
137+
POSTGRES_DB: postgres
138+
ports:
139+
- 5432:5432
140+
# Wait for the final TCP server, not the initialization socket server.
141+
options: >-
142+
--health-cmd "pg_isready -h 127.0.0.1 -p 5432 -U dev -d postgres"
143+
--health-interval 2s
144+
--health-timeout 5s
145+
--health-retries 30
115146
steps:
116147
- uses: actions/checkout@v6
117148

@@ -124,8 +155,5 @@ jobs:
124155
- name: Check migrations
125156
run: pnpm run db:check
126157

127-
- name: Create database
128-
run: pnpm run db:create
129-
130158
- name: Migrate database
131159
run: DB_URL=postgres://dev:root@localhost:5432/postgres pnpm run db:migrate

‎.github/workflows/setup_preview.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ jobs:
3232
- name: Migrate database
3333
run: pnpm run db:migrate
3434
- name: Seed database
35-
run: pnpm run db:seed
35+
# Seeding needs auth initialization, but does not reuse signed cookies.
36+
# Password hashes are independent of this temporary, process-only secret.
37+
run: BETTER_AUTH_SECRET="$(openssl rand -base64 32)" pnpm run db:seed
3638
- name: Setup Juno File Storage
3739
run: pnpm run setupJunoFileStorage
3840
env:
@@ -56,3 +58,4 @@ jobs:
5658
JUNO_FILE_BUCKET_PREFIX: ${{ vars.JUNO_FILE_BUCKET_PREFIX }}
5759
SETUP_ORGANIZATION_ID: ${{ vars.SETUP_ORGANIZATION_ID }}
5860
SENDGRID_KEY: ${{ secrets.SENDGRID_KEY }}
61+
EMAIL_SENDER_DOMAIN: ${{ vars.EMAIL_SENDER_DOMAIN }}

‎AGENTS.md‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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

Comments
 (0)