A modern Next.js application integrated with Ministry Platform authentication and REST API, built with TypeScript, Next.js 16, React 19, and Better Auth.
A High severity vulnerability (CVSS 8.1) let any authenticated user reassign their session to another user's Ministry Platform identity — inheriting that user's roles and groups, and forging their attribution in
dp_Audit_Log. Fixed in436466d.The usual advice is inverted here. MPNext is forked and copied, not installed, so the affected set is a commit range and nothing will alert you automatically:
- Affected — your fork contains
c9d80d4(2026-07-09) but not436466d.- Not affected — your fork predates
c9d80d4. Do not "update to latest" reflexively; merging pastc9d80d4without the fix would introduce the flaw.git merge-base --is-ancestor c9d80d4 HEAD && echo "has the flaw" git merge-base --is-ancestor 436466d HEAD && echo "has the fix"Patching does not revoke sessions already forged — they survive in the JWT cookie cache for up to an hour. See the full advisory for verification steps, incident response, and
dp_Audit_Logguidance.
- Security Notice
- Features
- Architecture
- Prerequisites
- Getting Started
- Project Structure
- Ministry Platform Integration
- Components
- Services
- Testing
- Development
- Claude Code Commands
- Documentation
- Code Style & Conventions
- Known Issues
- Contributing
- License
- Support
- Authentication: Better Auth with Ministry Platform OAuth (via genericOAuth plugin) and OIDC RP-initiated logout
- Modern UI: Radix UI primitives + shadcn/ui components with Tailwind CSS v4
- Type-Safe API: Full TypeScript support with auto-generated types from Ministry Platform schema
- Next.js 16: App Router with React Server Components and Turbopack
- REST API Client: Comprehensive Ministry Platform REST API integration
- Type Generation: CLI tool to generate TypeScript interfaces and Zod schemas from MP database
- Schema Documentation: Auto-generated markdown documentation with type file links
- Validation: Optional Zod v4 schema validation in MPHelper for runtime data validation before API calls
- Authorization: Ministry Platform security-role gate on reads and writes, not just an authentication check
- Security Headers: Nonce-based Content-Security-Policy, enforcing by default, applied per-request in the proxy
- Audit Attribution: Writes carry the acting user's MP
User_ID, sodp_Audit_Logrecords who actually did what - Testing: Vitest with 1,015 tests and enforced coverage thresholds in CI
- Next.js 16 with App Router and Turbopack (default bundler for dev and build)
- React 19 with Server Components by default
- TypeScript in strict mode
- Tailwind CSS v4 for styling
- Vitest 4 for testing, with coverage thresholds gated in CI
Custom provider located at src/lib/providers/ministry-platform/ featuring:
- REST API client with OAuth2 authentication
- Service-oriented architecture for domain-specific logic
- Type-safe models and Zod validation schemas for all 301 MP tables (603 generated files)
- Automatic token management with refresh
- Six specialized services: Table, Procedure, Communication, File, Metadata, Domain
Better Auth with Ministry Platform OAuth via genericOAuth plugin (src/lib/auth.ts)
- Stateless JWT cookie sessions (no database required)
- Session enrichment via
customSession(name split plus the MPUser_ID); the fullMPUserProfileis loaded client-side byUserProvider - OIDC RP-initiated logout for proper session termination
- Security-role authorization via
AuthorizationService, gating reads as well as writes - Proxy-based route protection (
src/proxy.ts— Next.js 16 replaces middleware with proxy) - Client-side auth via
authClient(src/lib/auth-client.ts)
- Node.js: v20 LTS or higher (Next.js 16 and React 19 require a modern Node runtime).
npm run setupenforces this minimum; CI builds and tests on Node 22. - Package Manager: npm (comes with Node.js)
- Ministry Platform: Active instance with API credentials and OAuth client configured (see OAuth Setup)
Run npm run setup for an interactive guided setup, or follow the Manual Setup steps below. The setup script is a plain Node/tsx script — Claude Code is not required.
git clone https://github.com/MinistryPlatform-Community/MPNext.git
cd MPNext
npm install
npm run setupThe interactive setup command will:
- Verify Node.js version (v20 LTS+ required)
- Detect a template clone and offer to keep, reinitialize, or set a new git origin
- Check git status
- Create
.env.localfrom.env.example(if needed) - Prompt for missing environment variables — derives both
MINISTRY_PLATFORM_BASE_URLandNEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URLfrom a single MP host input (avoiding the copy/paste mismatch risk in the Manual Setup path), and offers to auto-generateBETTER_AUTH_SECRET - Install dependencies (
npm install) - Apply non-breaking updates (
npm update) - Generate Ministry Platform types
- Run a production build to verify configuration
If you intend to commit
package-lock.jsonafterwards, runnpm run deps:relockfirst. Thenpm updatein step 7 dedupes as a side effect, which on Windows produces a lockfile that fails CI on Linux — see Known Issues.
Additional setup options:
npm run setup:check # Validation only (no changes)
npm run setup -- --clean # Clean install (delete node_modules first)
npm run setup -- --skip-install # Skip npm install/update
npm run setup -- --verbose # Extra output
npm run setup -- --help # Show all optionsOnce setup completes, run npm run dev and visit http://localhost:3000.
For a manual setup, follow these steps:
git clone https://github.com/MinistryPlatform-Community/MPNext.git
cd MPNextnpm install
npm update # Apply non-breaking patch/minor updates (kept here to mirror the interactive setup flow)Note: The interactive
npm run setupflow runsnpm updateautomatically. Running it here keeps the manual and automated flows aligned.
Do not relock casually. The commands above are fine for a fresh clone, but if you need to regenerate
package-lock.json, usenpm run deps:relock— a barenpm install/npm dedupeon Windows produces a lockfile that fails CI on Linux. See Known Issues.
Copy the example environment file and configure it with your Ministry Platform credentials:
cp .env.example .env.localUpdate .env.local with your configuration:
# Better Auth Configuration (used for end-user OAuth login)
OIDC_CLIENT_ID=TM.Widgets
OIDC_CLIENT_SECRET=your_client_secret
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET=your_generated_secret
# Update for production
BETTER_AUTH_URL=http://localhost:3000
# MinistryPlatform API Configuration (used for server-side API access)
MINISTRY_PLATFORM_CLIENT_ID=MPNext
MINISTRY_PLATFORM_CLIENT_SECRET=your_client_secret
MINISTRY_PLATFORM_BASE_URL=https://your-instance.ministryplatform.com/ministryplatformapi
# Authorization — comma-separated MP security role names allowed to use the
# contact features (reads AND writes). Blank means any MP security role will do.
MP_SECURITY_ROLES=
# Public Keys
NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL=https://your-instance.ministryplatform.com/ministryplatformapi/files
NEXT_PUBLIC_APP_NAME=MPNextApp
# Security headers — leave blank. The Content-Security-Policy ENFORCES by
# default; only the exact string "false" drops it back to report-only.
CSP_ENFORCE=
MP_SECURITY_ROLESis the access control. Leaving it blank means any Ministry Platform security role can use the gated features. A signed-in user holding no security role at all can still see the app shell but is redirected to/no-access. A deprecated write-only predecessor,MP_WRITE_SECURITY_ROLES, is still honored whenMP_SECURITY_ROLESis unset — so an existing deployment is not silently widened — but it now governs reads too. New deployments should set onlyMP_SECURITY_ROLES. See.claude/references/auth.mdfor the full policy.
CSP_ENFORCEis inverted on purpose. Anything other than the literal stringfalse— including leaving it unset — enforces the policy. A typo therefore fails loud (too strict) rather than silent (no policy at all). Set it tofalseonly to diagnose a violation.
Note:
OIDC_CLIENT_IDandMINISTRY_PLATFORM_CLIENT_IDmay be the same value or different. The example above uses the common pattern of sharing theTM.WidgetsOAuth client for congregant-facing login and a scoped server-side client (MPNext) for API access. See the comments in.env.examplefor details.
Before running the application, you must configure an OAuth 2.0 / OpenID Connect (OIDC) client in Ministry Platform.
Log in to your Ministry Platform instance as an administrator and navigate to Administration > API Clients.
Create a new API Client with the following configuration:
- Client ID:
MPNext(or your custom client ID) - Client Secret: Generate a secure secret (save this securely - you'll need it for
.env.local) - Display Name:
MPNext(or your preferred name) - Client User: Create a scoped user or use API User
- Authentication Flow: use the default: Authorization Code, Implicit, Hybrid, Client Credentials, or Resource Owner
Add these authorized redirect URIs where users will be sent after authentication - separate each entry by ending with a semi-colon(;):
Development:
http://localhost:3000/api/auth/callback/ministry-platform
Production:
https://yourdomain.com/api/auth/callback/ministry-platform
Important: The redirect URI must match exactly (including protocol, domain, port, and path). Ministry Platform will reject any OAuth requests with mismatched redirect URIs. The callback path follows Better Auth's social-provider convention:
/api/auth/callback/{providerId}.
Upgrading from a pre-1.7 Better Auth setup? The callback path changed in Better Auth 1.7, from
/api/auth/oauth2/callback/ministry-platformto/api/auth/callback/ministry-platform. Add the new URI to the Ministry Platform OAuth client before deploying; keep the old one until every environment is upgraded, then remove it.
Add these URIs where users will be redirected after signing out:
Development:
http://localhost:3000
Production:
https://yourdomain.com
Important: Post-logout redirect URIs are required for proper logout functionality. The application implements OIDC RP-initiated logout to properly end Ministry Platform OAuth sessions. Without these configured, users will be auto-logged back in after clicking "Sign out" (SSO behavior).
Generate a secure secret for Better Auth session signing (must be at least 32 characters):
openssl rand -base64 32Copy the generated secret to your .env.local file as BETTER_AUTH_SECRET.
Before running the application, generate TypeScript types from your Ministry Platform database schema:
npm run mp:generate:modelsThis will:
- Connect to your Ministry Platform API
- Fetch all table metadata (301+ tables)
- Generate TypeScript interfaces for each table
- Generate Zod validation schemas for runtime validation
- Generate schema documentation with type file links
- Clean up any previously generated files
- Output to
src/lib/providers/ministry-platform/models/
Expected output:
Generating TypeScript types from Ministry Platform schema...
Fetching table metadata from Ministry Platform...
Found 301 tables
Cleaning output directory: src/lib/providers/ministry-platform/models
Removed 605 existing type files
Generating type definitions...
Contacts.ts (Contacts) [51 columns]
Events.ts (Events) [57 columns]
...
Successfully generated 301 table types + 301 Zod schemas (602 total files)
Advanced options:
# Generate types for specific tables only
npx tsx src/lib/providers/ministry-platform/scripts/generate-types.ts -s "Contact"
# Generate without Zod schemas
npx tsx src/lib/providers/ministry-platform/scripts/generate-types.ts -o ./types
# Generate with detailed mode (samples records for better type inference)
npx tsx src/lib/providers/ministry-platform/scripts/generate-types.ts -d --sample-size 10
# See all options
npx tsx src/lib/providers/ministry-platform/scripts/generate-types.ts --helpNote: Field names containing special characters (like
Allow_Check-in) are automatically quoted in the generated types for valid TypeScript syntax.
Start the development server and test the authentication flow:
npm run dev- Navigate to http://localhost:3000
- Click "Sign In"
- You should be redirected to Ministry Platform login
- After successful login, you'll be redirected back to the application
- Your session should be active
Troubleshooting:
- "Redirect URI mismatch": Verify redirect URI in MP matches exactly
- "Invalid client": Check client ID and secret are correct
- "Unauthorized scope": Ensure all required scopes are enabled
- Auto-login after logout: Verify post-logout redirect URIs are configured in Ministry Platform OAuth client. The application requires these for proper OIDC logout (see OAUTH_LOGOUT_SETUP.md)
When deploying to production:
- Update
BETTER_AUTH_URLto your production domain - Add production redirect URI (
https://yourdomain.com/api/auth/callback/ministry-platform) to Ministry Platform OAuth client - Add production post-logout redirect URIs
- Ensure environment variables are set in your hosting provider
- Enable HTTPS/SSL certificates
- Test the complete authentication flow in production environment
MPNext/
├── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── (web)/ # Protected route group
│ │ │ ├── contactlookup/ # Contact lookup demo
│ │ │ │ ├── [guid]/ # Dynamic contact detail page
│ │ │ │ ├── layout.tsx # Authorization gate (redirects to /no-access)
│ │ │ │ └── page.tsx # Search page
│ │ │ ├── home/ # Home redirect
│ │ │ ├── no-access/ # Shown when the role gate denies
│ │ │ ├── error.tsx # Shell-preserving error boundary
│ │ │ ├── layout.tsx # Web layout with auth
│ │ │ └── page.tsx # Dashboard/home page
│ │ ├── api/auth/[...all]/ # Better Auth API routes (deny-by-default allowlist)
│ │ ├── auth-error/ # Failed MP OAuth callback landing page
│ │ ├── session-error/ # Recovery for a session with no userGuid
│ │ ├── signin/ # Sign-in page
│ │ ├── error.tsx # Root segment error boundary
│ │ ├── global-error.tsx # Replaces the document on a root throw
│ │ ├── layout.tsx # Root layout
│ │ └── providers.tsx # App providers wrapper
│ │
│ ├── components/ # React components
│ │ ├── contact-logs/ # Contact logs feature (CRUD)
│ │ ├── contact-lookup/ # Contact lookup feature
│ │ │ ├── contact-lookup.tsx
│ │ │ ├── contact-lookup-search.tsx
│ │ │ ├── contact-lookup-results.tsx
│ │ │ ├── actions.ts
│ │ │ └── index.ts
│ │ ├── contact-lookup-details/ # Contact details feature
│ │ ├── home-demos/ # Dashboard demo cards
│ │ ├── sign-in/ # Sign-in button / OAuth kickoff
│ │ ├── user-menu/ # User menu feature
│ │ ├── layout/ # Layout components
│ │ │ ├── auth-wrapper.tsx
│ │ │ ├── dynamic-breadcrumb.tsx
│ │ │ ├── header.tsx
│ │ │ ├── sidebar.tsx
│ │ │ └── index.ts
│ │ ├── shared-actions/ # Cross-feature server actions
│ │ │ ├── domain.ts
│ │ │ ├── user.ts
│ │ │ └── README.md
│ │ └── ui/ # shadcn/ui components (19 components)
│ │
│ ├── contexts/ # React Context providers
│ │ ├── session-context.tsx # useAppSession()
│ │ ├── user-context.tsx # UserProvider / useUser()
│ │ └── index.ts
│ │
│ ├── lib/ # Shared libraries
│ │ ├── auth.ts # Better Auth server configuration
│ │ ├── auth-client.ts # Better Auth client (React hooks)
│ │ ├── security-headers.ts # CSP + security header policy
│ │ ├── dto/ # Application DTOs/ViewModels
│ │ │ ├── contacts.ts
│ │ │ ├── contact-logs.ts
│ │ │ └── index.ts
│ │ ├── utils.ts # General utilities
│ │ └── providers/
│ │ └── ministry-platform/ # Ministry Platform provider
│ │ ├── auth/ # OAuth client-credentials flow
│ │ ├── services/ # API services
│ │ │ ├── table.service.ts
│ │ │ ├── procedure.service.ts
│ │ │ ├── communication.service.ts
│ │ │ ├── file.service.ts
│ │ │ ├── metadata.service.ts
│ │ │ └── domain.service.ts
│ │ ├── models/ # Generated: 301 types + 301 Zod schemas + barrel
│ │ ├── types/ # Type definitions
│ │ ├── utils/ # HTTP client, filter sanitization
│ │ ├── scripts/ # Type / stored-proc generation CLI
│ │ ├── docs/ # Provider documentation
│ │ ├── client.ts # Core MP client
│ │ ├── provider.ts # Singleton provider
│ │ ├── helper.ts # Public API (MPHelper)
│ │ └── index.ts # Barrel export
│ │
│ ├── services/ # Application services
│ │ ├── authorizationService.ts # MP security-role gate (reads and writes)
│ │ ├── contactService.ts
│ │ ├── contactLogService.ts
│ │ ├── domainTimezoneService.ts # MP datetime boundary conversion
│ │ ├── sessionContextService.ts # Acting MP User_ID for audit attribution
│ │ └── userService.ts
│ │
│ ├── proxy.ts # Next.js 16 proxy (route protection)
│ └── test-setup.ts # Vitest setup
│
├── .claude/ # Claude AI configuration
│ ├── commands/ # Claude Code skills (/audit-deps, /release)
│ ├── references/ # Documentation references
│ ├── playbooks/ # Porting playbooks
│ └── reports/ # Past dependency audit reports
├── .githooks/ # pre-commit lockfile guard
├── .github/workflows/ # CI: tests + lockfile drift check
├── docs/
│ ├── OAUTH_LOGOUT_SETUP.md
│ └── security/ # Security advisories
├── scripts/ # setup.ts, check-lockfile.mjs
├── public/ # Static assets
├── coverage/ # Test coverage reports
├── .env.example # Environment template
├── CLAUDE.md # Development guide
├── eslint.config.mjs # Flat ESLint config (incl. no-console rule)
├── vitest.config.mts # Vitest configuration + coverage gates
├── components.json # shadcn/ui configuration
├── next.config.ts # Next.js configuration
├── tailwind.config.js # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies and scripts
Nearly every source file has a co-located
*.test.ts(x)beside it; they are omitted from this tree for readability.
The main entry point for interacting with Ministry Platform:
import { MPHelper } from '@/lib/providers/ministry-platform';
import { ContactLogSchema } from '@/lib/providers/ministry-platform/models';
import { DomainTimezoneService } from '@/services/domainTimezoneService';
const mp = new MPHelper();
const tz = DomainTimezoneService.getInstance();
// Get contacts with query parameters
const contacts = await mp.getTableRecords({
table: 'Contacts',
filter: 'Contact_Status_ID=1',
select: 'Contact_ID,Display_Name,Email_Address',
orderBy: 'Last_Name',
top: 50
});
// Create records (without validation - backward compatible)
// Note: datetimes crossing the MP boundary go through DomainTimezoneService.
// MP stores wall-clock values in the domain's time zone, NOT UTC, so a raw
// `new Date().toISOString()` writes the wrong instant.
await mp.createTableRecords('Contact_Log', [{
Contact_ID: 12345,
Contact_Date: await tz.toMpSqlDatetime(new Date()),
Made_By: 1,
Notes: 'Follow-up call completed'
}]);
// Create records with Zod validation (recommended)
await mp.createTableRecords('Contact_Log', [{
Contact_ID: 12345,
Contact_Date: await tz.toMpSqlDatetime(new Date()),
Made_By: 1,
Notes: 'Follow-up call completed'
}], {
schema: ContactLogSchema, // Validates data before API call
$userId: 1
});
// Update with partial validation (default)
await mp.updateTableRecords('Contact_Log', records, {
schema: ContactLogSchema,
partial: true // Allow partial updates
});
// Note: in real code `Made_By` and `Contact_ID` are server-authoritative —
// ContactLogService derives them from the authorized session, never from the
// caller. They are shown literally here only to keep the example self-contained.
// Execute stored procedures
const results = await mp.executeProcedureWithBody('api_Custom_Procedure', {
'@ContactID': 12345
});
// File operations
const files = await mp.getFilesByRecord({
table: 'Contacts',
recordId: 12345
});| Service | Purpose | Key Methods |
|---|---|---|
| Table Service | CRUD operations | getTableRecords, createTableRecords, updateTableRecords, deleteTableRecords |
| Procedure Service | Stored procedures | getProcedures, executeProcedure, executeProcedureWithBody |
| Communication Service | Email/SMS | createCommunication, sendMessage |
| File Service | File management | getFilesByRecord, uploadFiles, updateFile, deleteFile, getFileContentByUniqueId, getFileMetadata, getFileMetadataByUniqueId |
| Metadata Service | Schema info | getTables, refreshMetadata |
| Domain Service | Domain config | getDomainInfo, getGlobalFilters |
Generate TypeScript interfaces and Zod schemas from your Ministry Platform database schema:
# Generate types for all tables with Zod schemas (recommended)
npm run mp:generate:models
# Generate types for specific tables
npx tsx src/lib/providers/ministry-platform/scripts/generate-types.ts --search "Contact"
# Generate to a custom directory with Zod schemas
npx tsx src/lib/providers/ministry-platform/scripts/generate-types.ts -o ./types --zod
# See all options
npx tsx src/lib/providers/ministry-platform/scripts/generate-types.ts --helpCLI Options:
-o, --output <dir>- Output directory (default: ./generated-types)-s, --search <term>- Filter tables by search term-z, --zod- Generate Zod schemas for runtime validation-c, --clean- Remove existing files before generating (recommended)-d, --detailed- Sample records for better type inference (slower)--sample-size <num>- Number of records to sample in detailed mode-h, --help- Show the help message
Generated Output:
- 301 TypeScript interfaces (one per table)
- 301 Zod validation schemas
- A barrel
index.tsre-exporting all of them (603 files in total) - Schema documentation with type file links (
.claude/references/ministryplatform.schema.md)
See Ministry Platform Type Generator documentation for details.
Built with Radix UI primitives and styled with Tailwind CSS. Located in src/components/ui/:
- Alert, Alert Dialog, Avatar, Breadcrumb, Button, Card
- Checkbox, Dialog, Drawer, Dropdown Menu, Form, Input
- Label, Radio Group, Select, Skeleton, Switch, Textarea, Tooltip
- AuthWrapper: Server component for route protection with session validation
- Header: Application header with sidebar toggle and user menu
- Sidebar: Navigation sidebar with route links
- DynamicBreadcrumb: Auto-generated breadcrumbs from URL path
- contact-lookup: Contact search by name, email or phone
- contact-lookup-details: Detailed contact view with logs
- contact-logs: Full CRUD for contact interaction history
- home-demos: Dashboard demo tiles, hidden for users without contact access
- sign-in: Sign-in page body; starts the OAuth flow with callback-URL sanitization
- user-menu: User profile dropdown with sign-out
One throw no longer takes the whole page. Three boundaries, each scoped deliberately:
src/app/(web)/error.tsx— inside the protected group, so the header and sign-out survive a feature crashsrc/app/error.tsx— the bare recovery routes outside the app shellsrc/app/global-error.tsx— last resort when the root layout itself throws; it renders its own<html>and inlines its styles
All components follow kebab-case naming and use named exports. Files in src/app/ (page.tsx, layout.tsx, error.tsx, global-error.tsx) use default exports because the App Router requires it.
Application services provide business logic abstraction over the Ministry Platform API:
| Service | File | Purpose |
|---|---|---|
| ContactService | contactService.ts |
Contact search and updates |
| ContactLogService | contactLogService.ts |
Contact log CRUD with validation |
| UserService | userService.ts |
User profile retrieval |
| AuthorizationService | authorizationService.ts |
Ministry Platform security-role gate for reads and writes; throws UnauthorizedError |
| SessionContextService | sessionContextService.ts |
Resolves the acting MP User_ID so writes are attributed correctly in dp_Audit_Log |
| DomainTimezoneService | domainTimezoneService.ts |
Converts every datetime crossing the MP boundary, since MP stores wall-clock values in the domain's time zone |
All services follow the singleton pattern; all except SessionContextService use MPHelper for API communication.
Authorize, don't just authenticate. A session only proves that some Ministry Platform user signed in. All MP data is fetched with the application's service account, so
AuthorizationService.requireSecurityRoleis the only thing deciding who may see or change a record — and it gates reads as well as writes. Server actions and service methods must call it rather than a bareauth.api.getSession()check.
The project uses Vitest 4 — 1,015 tests at 99.74% statement coverage, gated in CI.
- Framework: Vitest with jsdom environment
- Libraries: @testing-library/react, @testing-library/jest-dom
- Coverage: v8 provider with HTML reports
# Run tests in watch mode
npm test
# Single test run
npm run test:run
# Generate coverage report
npm run test:coverageTests are co-located with the code they cover — foo.ts sits next to foo.test.ts.
| Area | Coverage |
|---|---|
| Overall | 99.74% statements, 97.21% branches (gated in CI) |
Services (src/services/) |
100% |
Server actions (**/actions.ts) |
100% |
App routes (src/app/**) |
100% |
| React components | 99.69% |
| MP provider + sub-services | 99.72% |
Total: 1,015 tests across 59 test files — 99.74% statements, 97.21% branches, 99.31% functions, 99.91% lines.
Tests are configured in vitest.config.mts:
- Environment variables stubbed in
src/test-setup.ts - Auto-generated models and the dev-only codegen scripts are excluded from coverage
src/components/ui/is excluded from the denominator — testing thin shadcn/Radix wrappers only asserts that Radix works- Supports TypeScript path aliases
Coverage is gated, not advisory. vitest.config.mts sets per-area thresholds (src/app/**, src/components/**/*.tsx, src/services/**, src/lib/**/*.ts, src/contexts/**, and src/proxy.ts at 100%) plus a global backstop of 98% statements / 95% branches / 97% functions / 98% lines. The global gate is what catches a newly added, entirely untested file, since a new file inside a per-area glob would simply be diluted by everything already covered there.
CI (.github/workflows/test.yml) runs npx vitest run --coverage on Node 22, alongside a separate lockfile job described under Known Issues. The Codecov upload is pinned fail_ci_if_error: false and cannot fail the build — the coverage thresholds are the actual PR gate. CI runs neither tsc nor eslint, so run those locally.
# Start development server
npm run dev
# Build for production (Turbopack, includes type checking)
npm run build
# Start production server
npm start
# Run ESLint (native flat config — `next lint` was removed in Next.js 16)
npm run lint
# Run tests
npm test # Watch mode
npm run test:run # Single run
npm run test:coverage # With coverage report
# Generate MP types (basic, to custom location)
npm run mp:generate
# Generate MP types to models directory with Zod schemas (recommended)
npm run mp:generate:models
# Regenerate the stored-procedure reference
npm run mp:generate:storedprocs
# Dependencies — see Known Issues before touching the lockfile
npm run deps:verify # Check package-lock.json for platform drift
npm run deps:relock # The ONLY supported way to regenerate package-lock.json
# Interactive project setup
npm run setup
npm run setup:check # Validate setup without making changesnpm run build
npm startNote: The build process includes TypeScript type checking. Ensure all generated types are up to date by running
npm run mp:generate:modelsbefore building.
This project includes custom Claude Code commands (skills) to streamline development workflows. These commands are invoked using the /command syntax in Claude Code.
| Command | Description |
|---|---|
/audit-deps [args] |
Dependency audit — advisories, exploitability triage, and guided upgrades |
/release [args] |
Cut a GitHub release with notes auto-generated from merged PRs |
Performs a complete review of dependencies: real vulnerability exposure, available updates, and — with approval where required — applies and verifies the upgrades.
What it does:
- Runs
npm auditfor vulnerability detection - Triages each advisory for actual exploitability in this codebase, rather than treating the raw count as the signal
- Categorizes updates as safe (patch/minor) or major (breaking changes)
- Applies approved upgrades and verifies them against build, lint, and tests
Usage:
/audit-deps # Full audit, then guided upgrades
/audit-deps --report-only # Audit and report; change nothing
/audit-deps --majors-only # Only consider major-version upgrades
/audit-deps --major <pkg> # Take a specific package to its next major
/audit-deps --no-verify # Skip the post-upgrade verification run
This command never reads or writes Ministry Platform data — it touches only local dependencies and report files. Past run reports are kept in
.claude/reports/.
Creates a GitHub release with notes generated from the pull requests merged since the previous release.
What it does:
- Finds the last release tag and collects PRs merged since
- Computes a calver tag,
v{YYYY}.{MM}.{DD}.{HHmm}(UTC), unless you pass--tag - Groups PRs into Breaking Changes / Features / Bug Fixes / Documentation / Maintenance
- Shows you the draft notes for approval before publishing
Usage:
/release # Auto-compute the calver tag
/release --tag v2026.09.12.1200 # Use an explicit tag
Command definitions are stored in .claude/commands/:
.claude/commands/
├── audit-deps.md # Dependency audit command
└── release.md # GitHub release command
- CLAUDE.md - Development guide with commands, architecture, and code style conventions
- OAUTH_LOGOUT_SETUP.md - OAuth logout configuration and OIDC RP-initiated logout details
- Ministry Platform Provider - Complete provider documentation
- Type Generator - CLI tool documentation
- Components Reference - Detailed component and route inventory with compliance status
- MP Schema Reference - Auto-generated database schema
- MP Stored Procedures - Auto-generated stored-procedure inventory
- MP Query Syntax - Filters, aggregates, and FK traversal rules for
GET /tables/{table} - MP Date/Time Handling - Sending and receiving MP datetimes safely via
DomainTimezoneService - Auth Reference - Better Auth config, OAuth flow, the security-role gate, and session access patterns
- Testing Reference - Vitest setup, mock patterns, coverage data, and test inventory
- Security Headers - The nonce-based CSP and the deliberate loosenings not to "tighten"
- Dependency Known Issues - Lockfile platform drift and standing advisories
- Session Identity Advisory - The 2026-09-12 vulnerability, who is affected, and remediation
Use the @/* path alias for all internal imports:
import { MPHelper } from '@/lib/providers/ministry-platform';
import { Button } from '@/components/ui/button';
import { ContactSearch } from '@/lib/dto';
import { Header, Sidebar } from '@/components/layout';- React Server Components by default
- Add
"use client"only when needed for interactivity - Keep UI components in
src/components/ui/ - Follow shadcn/ui conventions
- Use named exports (no default exports) — except
src/app/route files, which the App Router requires to default-export - Organize feature components in folders with barrel exports
- PascalCase: Component names, types, interfaces
- camelCase: Functions, variables
- kebab-case: All component files and folders
- snake_case: Ministry Platform API fields
src/components/
├── shared-actions/ # Cross-feature server actions
├── ui/ # shadcn/ui components
├── layout/ # Layout components (header, sidebar, etc.)
├── feature-name/ # Feature folder (kebab-case)
│ ├── feature-name.tsx # Main component
│ ├── actions.ts # Feature-specific server actions
│ └── index.ts # Barrel exports
└── shared-component.tsx # Shared standalone components
// Import feature components via barrel exports
import { ContactLookup } from '@/components/contact-lookup';
import { UserMenu } from '@/components/user-menu';
// Import layout components
import { Header, Sidebar, AuthWrapper } from '@/components/layout';
// Import application DTOs
import { ContactSearch, ContactLookupDetails } from '@/lib/dto';
// Import Ministry Platform models (generated)
import { ContactLog, Congregations } from '@/lib/providers/ministry-platform/models';
// Import Ministry Platform Zod schemas
import { ContactLogSchema } from '@/lib/providers/ministry-platform/models';
// Import Ministry Platform helper
import { MPHelper } from '@/lib/providers/ministry-platform';
// Import shared actions
import { getCurrentUserProfile } from '@/components/shared-actions/user';- Strict mode enabled
- Export interfaces from models
- Use Zod schemas for validation
- Leverage TypeScript generics for type safety
- Regenerate types after Ministry Platform schema changes:
npm run mp:generate:models - Always use TypeScript generics for type-safe API calls
- Handle errors with try-catch blocks
- Use Zod schemas for runtime validation - Pass the optional
schemaparameter tocreateTableRecords()andupdateTableRecords()to validate data before API calls:import { ContactLogSchema } from '@/lib/providers/ministry-platform/models'; await mp.createTableRecords('Contact_Log', records, { schema: ContactLogSchema, // Catch validation errors before API call $userId: 1 });
- Keep Ministry Platform structure organized:
- Generated database models:
src/lib/providers/ministry-platform/models/(auto-generated, don't edit manually) - Application-level DTOs/ViewModels:
src/lib/dto/(hand-written) - Export all from respective
index.tsfiles
- Generated database models:
- Access fields with special characters using bracket notation:
event["Allow_Check-in"] - Run tests before committing:
npm run test:run
Never regenerate package-lock.json with a bare npm install or npm dedupe on Windows. Use npm run deps:relock.
This repo's lockfile is authored on Windows and installed by CI on Linux. npm resolves optional and bundled subtrees per platform, so a Windows-generated lockfile can omit entries that npm ci on Linux requires — CI then dies at the install step before a single test runs. This broke main twice (2026-05-17 @emnapi/*, 2026-08-21 ajv). One npm dedupe on Windows is enough to reproduce it.
Critically, npm ci --dry-run cannot detect this on Windows. It exits 0 there against a lockfile that fails on Linux, and --os/--cpu do not change that. A green local check proves nothing. Run npm run deps:verify, which asserts the lockfile already matches Linux resolution.
Two things enforce this: a pre-commit hook (.githooks/pre-commit, auto-installed by the prepare script) and a dedicated lockfile job in CI. Full detail: Dependency Known Issues.
Also: do not run npm ci while next dev is running. It deletes node_modules first, then aborts on a locked native .node file, leaving the tree half-installed. Stop the dev server first.
npm audit currently reports 0 vulnerabilities across 720 packages (verified 2026-09-12). The moderate postcss advisories this section used to describe were resolved upstream: next@16.3.5 bundles postcss@8.5.23 and the top-level postcss resolves to 8.5.28, both well clear of the < 8.5.10 threshold.
Never run npm audit fix --force. It still "fixes" bundled-dependency findings by downgrading next to a major version this codebase cannot run on.
Re-check with /audit-deps, which triages each finding for real exploitability rather than treating the raw count as the signal. Triaged vendor advisories that npm audit does not surface are tracked in Dependency Known Issues; past run reports are kept in .claude/reports/.
This project follows strict TypeScript conventions and code style. Please review CLAUDE.md before contributing.
Private
For Ministry Platform API documentation, refer to your instance's API documentation portal.