A Ministry Platform page tools application powered by Next.js 16, React 19, Better Auth, and a Ministry Platform REST API integration with TypeScript and Zod validation.
MPNext-Tools is a template: churches fork or copy it and run their own instance. If you run a copy, read .github/SECURITY.md — fixes made here do not reach your fork automatically.
- Features
- Architecture
- Prerequisites
- Getting Started
- Project Structure
- Tools
- Ministry Platform Integration
- Components
- Services
- Testing
- Development
- Claude Code Commands
- Documentation
- Code Style & Conventions
- Security
- Authentication: Better Auth with Ministry Platform OAuth/OIDC (genericOAuth plugin), stateless encrypted-cookie sessions, and OIDC RP-initiated logout
- Authorization: every MP read and write is gated by MP security role (
MP_SECURITY_ROLES, fails closed); users without an allowed role land on/no-access - Tools Framework: reusable components for building Ministry Platform page tools, with URL parameter parsing and create/edit dual mode
- Address Labels: print labels with USPS Intelligent Mail Barcodes (IMb), POSTNET fallback, and Word document mail merge
- Group Wizard: six-step create/edit wizard for MP
Groupsrecords - Field Management: drag-and-drop editor for MP page field order, groups and per-field flags
- Add/Edit Family: household editor with contact search, address autocomplete (Google Places) and envelope numbering
- Type-Safe API: TypeScript strict mode with model and Zod v4 schema files generated from your MP instance
- Security hardening: nonce-based CSP, security headers, boot-time refusal of weak secrets and malformed URLs, sign-in rate limiting
- Testing: Vitest 5 suite with enforced coverage thresholds (see Testing)
- Setup Wizard: interactive CLI for environment configuration, dependency install, type generation and build verification
- Next.js 16 with App Router and Turbopack (default bundler for dev and build)
- React 19 with Server Components by default
- TypeScript 6.0 in strict mode
- Tailwind CSS v4 with the
@tailwindcss/postcssplugin - Zod v4 for runtime validation
- Vitest 5 with jsdom and v8 coverage
Custom provider at src/lib/providers/ministry-platform/ with a layered architecture:
MPHelper (Public API Facade)
-> MinistryPlatformProvider (Singleton Orchestrator)
-> MinistryPlatformClient (HTTP + OAuth2 Token Management)
-> Six Specialized Services
-> HttpClient (Raw HTTP with Bearer Auth)
- REST API client using OAuth2 client credentials; the token is re-fetched every 5 minutes, well inside its 1-hour lifetime
- Type-safe models and Zod validation schemas generated from the MP schema
- Six services: Table, Procedure, Communication, File, Metadata, Domain
Better Auth with Ministry Platform OAuth via the genericOAuth plugin (src/lib/auth.ts):
- One MP API client (
MINISTRY_PLATFORM_CLIENT_ID) serves as both the server-side service account and the sign-in client - Stateless sessions (no database): hard 12-hour lifetime, encrypted (JWE) cookie cache, no user OAuth tokens stored, and boot-time refusal of a weak/missing
BETTER_AUTH_SECRETor malformed base URLs - OIDC RP-initiated logout for proper MP session termination
- Layered protection:
src/proxy.ts— session-cookie check on every non-public route (plus the CSP nonce)AuthWrapper— server-side session validation; redirects to/signin, or to/session-errorfor a session with nouserGuidrequireToolAccess()— every tool page checks the caller's MP security role before parsing params or loading dataAuthorizationService.requireSecurityRole()— every server action and service method that touches MP data, reads included
- Client-side profile:
UserProviderloads the signed-in user's display profile (name, nickname, email) - Critical:
session.user.idis Better Auth's internal ID. Usesession.user.userGuidfor all MP lookups.
Component -> Server Action -> Service (singleton) -> MPHelper -> Ministry Platform API
- Node.js: v24.15.0 or later on the 24.x line (pinned via
engines.nodeinpackage.json,.nvmrc, and the setup script). Node 24 is the current LTS and the version Vercel deploys; 20.x and 22.x are not supported. Usenvm use(orfnm use) to pick it up from.nvmrc. - Package Manager: npm
- Ministry Platform: active instance with an API client configured (see OAuth Client Setup)
- Ministry Platform Database: SQL install script applied (see Database Setup)
The repo ships a bundled SQL install script at _INSTALL/ministryplatform-install.sql. It contains the stored procedures and DDL several tools depend on (for example, Field Management requires api_MPNextTools_GetPages, api_MPNextTools_GetPageFields, and api_MPNextTools_UpdatePageFieldOrder).
Deploy it once per Ministry Platform database (and again whenever the SQL bundle changes):
- Open SQL Server Management Studio (SSMS) and connect to the MP database
- Open
_INSTALL/ministryplatform-install.sql - Execute the script against the MP database
The script is regenerated automatically as part of npm run build (via the same script as npm run mp:build:install), bundling everything under src/lib/providers/ministry-platform/db/ into the single install file.
The interactive setup wizard automates the configuration:
git clone <repository-url>
cd MPNext-Tools
npm install
npm run setupThe setup wizard will:
- Verify the Node.js version (24.x, at least 24.15.0)
- Detect the project origin (template clone → convert to a fork or reinitialize git)
- Check git status
- Create
.env.localfrom.env.example(if needed) - Validate environment variables — prompts for the MP host (deriving
MINISTRY_PLATFORM_BASE_URLandNEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL), the API client ID/secret, andMP_SECURITY_ROLES, and auto-generatesBETTER_AUTH_SECRET - Run
npm install - Run
npm update - Regenerate Ministry Platform types against your MP instance
- Generate the stored procedure reference (powers Claude Code suggestions)
- Run a production build to verify configuration
When the wizard finishes it prints a reminder pointing to _INSTALL/ministryplatform-install.sql so you can deploy the SQL prerequisites to your MP database.
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.
git clone <repository-url>
cd MPNext-Tools
npm installnpm install also runs the prepare script, which points git at .githooks/ — its pre-commit hook refuses to commit any .env* file other than .env.example.
cp .env.example .env.localSet the required variables in .env.local. .env.example documents every variable inline.
# Generate with: openssl rand -base64 32 (at least 32 characters)
BETTER_AUTH_SECRET=your_generated_secret
# An origin only; https:// for every real host (http:// only for localhost)
BETTER_AUTH_URL=http://localhost:3000
# The app's one MP API client — service account AND sign-in client
MINISTRY_PLATFORM_CLIENT_ID=MPNext
MINISTRY_PLATFORM_CLIENT_SECRET=your_client_secret
MINISTRY_PLATFORM_BASE_URL=https://your-instance.ministryplatform.com/ministryplatformapi
# Who may use the tools. FAILS CLOSED: unset/blank = nobody.
# "*" = any MP security role; or a comma list, e.g. Administrators,Staff
MP_SECURITY_ROLES=*The app refuses to start (including next build) when BETTER_AUTH_SECRET is missing, shorter than 32 characters or Better Auth's public default; when BETTER_AUTH_SECRETS is set; or when a base URL is malformed or non-https. NEXTAUTH_SECRET / NEXTAUTH_URL are accepted as fallbacks for migrations from NextAuth.
Optional environment variables:
| Variable | Purpose |
|---|---|
MINISTRY_PLATFORM_DEV_CLIENT_ID / _SECRET |
Dev/staging-only MP API client, used only to run api_dev_* procedures from the dev panel's Deploy Tool. Ignored in production builds. |
GOOGLE_PLACES_API_KEY |
Address autocomplete in Add/Edit Family. The MP dp_Configuration_Settings value (COMMON / GoogleMapsAPIKey) wins; this variable is the fallback. Neither set = plain text input. |
NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL |
MP file URL — derived from the MP host by the setup wizard; also allowed by the CSP. |
NEXT_PUBLIC_PROD_URL |
Production URL used by the dev panel's Authorized Tools check. |
CSP_ENFORCE |
The CSP is enforced by default; only the exact string false switches to report-only. |
CSP_REPORT_URI |
Optional https:// CSP violation collector. |
AUTH_IP_ADDRESS_HEADERS |
Client-IP header(s) for sign-in rate limiting. Default on Vercel: x-vercel-forwarded-for,x-real-ip. |
AUTH_TRUSTED_PROXIES |
Proxy IPs/CIDRs to skip when walking an appended X-Forwarded-For chain. |
The repo ships pre-generated models under src/lib/providers/ministry-platform/models/ so a fresh clone builds immediately. Regenerate them against your own MP instance to pick up custom tables, columns, and constraints:
npm run mp:generate:modelsThis connects to your MP API, fetches all table metadata, and regenerates TypeScript interfaces and Zod v4 schemas into src/lib/providers/ministry-platform/models/ (the --clean flag removes stale files first). Run it whenever your MP schema changes.
Optionally, also refresh the stored procedure reference (used by Claude Code for query suggestions):
npm run mp:generate:storedprocsSee Type Generation for advanced options.
npm run dev- Navigate to http://localhost:3000
- You'll be redirected to Ministry Platform login
- After signing in you'll see the dashboard with the available tools (or
/no-accessif you hold none of the roles inMP_SECURITY_ROLES)
The app uses one MP API client for both server-side data access and user sign-in. In Ministry Platform, sign in as an administrator and go to Administration > API Clients.
- Client ID: must match
MINISTRY_PLATFORM_CLIENT_ID - Client Secret: generate a secure secret (→
MINISTRY_PLATFORM_CLIENT_SECRET) - Display Name: MPNext-Tools (or your preferred name)
- Client User: a scoped user (it is the audit-log user for writes made without a signed-in user)
Separate multiple entries with a semicolon (;). The path is <BETTER_AUTH_URL>/api/auth/callback/ministryplatform:
| Environment | URI |
|---|---|
| Development | http://localhost:3000/api/auth/callback/ministryplatform |
| Production | https://yourdomain.com/api/auth/callback/ministryplatform |
Exactly <BETTER_AUTH_URL>, with no trailing slash:
| Environment | URI |
|---|---|
| Development | http://localhost:3000 |
| Production | https://yourdomain.com |
Without a post-logout redirect URI, users are signed straight back in after signing out (SSO behavior).
- Deploy
_INSTALL/ministryplatform-install.sqlto the production MP database (see Database Setup) - Set every required variable in your hosting provider — including
MP_SECURITY_ROLES, which fails closed — withBETTER_AUTH_URLset to your production origin (https) - Add the production redirect and post-logout redirect URIs to the MP API client
- On a proxy other than Vercel, set
AUTH_IP_ADDRESS_HEADERS(orAUTH_TRUSTED_PROXIES) to match it - Run
npm run mp:generate:modelsso types match your instance - Test the complete sign-in, tool access and sign-out flow
MPNext-Tools/
├── .github/
│ ├── SECURITY.md # Vulnerability reporting policy
│ └── workflows/test.yml # CI: lint, typecheck, coverage, build, prerender check
├── .githooks/pre-commit # Blocks staged .env* files (except .env.example)
├── _INSTALL/ministryplatform-install.sql # Bundled MP SQL install (generated at build)
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (web)/ # Protected route group (AuthWrapper + Providers)
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx # Dashboard with tool cards
│ │ │ ├── error.tsx
│ │ │ ├── no-access/ # Users without an allowed MP security role
│ │ │ └── tools/
│ │ │ ├── layout.tsx
│ │ │ ├── require-tool-access.ts # Per-page role gate (every tool page calls it first)
│ │ │ ├── addeditfamily/ # Add/Edit Family (UI + actions co-located here)
│ │ │ ├── addresslabels/ # Address label printing
│ │ │ ├── fieldmanagement/ # MP page field layout editor
│ │ │ ├── groupwizard/ # Group create/edit wizard
│ │ │ └── template/ # Template tool (scaffold/demo)
│ │ ├── api/auth/[...all]/ # Better Auth handler (deny-by-default route allowlist)
│ │ ├── auth-error/ # OAuth callback failure page (public)
│ │ ├── session-error/ # Session without userGuid; sign-out escape hatch
│ │ ├── signin/ # OAuth sign-in page
│ │ ├── error.tsx, global-error.tsx # Error boundaries
│ │ ├── layout.tsx # Root HTML layout
│ │ └── providers.tsx # Context providers (UserProvider)
│ │
│ ├── components/ # See Component Organization below
│ ├── contexts/ # user-context.tsx, use-app-session.ts, index.ts
│ │
│ ├── lib/
│ │ ├── auth.ts, auth-client.ts # Better Auth server config / client hooks
│ │ ├── env.ts # Validated env access (getMpBaseUrl, getAuthBaseUrl, getMpClient)
│ │ ├── security-headers.ts # CSP + security headers
│ │ ├── tool-params.ts # Client-safe tool URL param types/helpers
│ │ ├── tool-params.server.ts # parseToolParams() (server-only)
│ │ ├── imb-encoder.ts, postnet-encoder.ts, barcode-helpers.ts, barcode-image.ts
│ │ ├── label-stock.ts # Label stock definitions (Avery 5160, etc.)
│ │ ├── constants.ts, utils.ts, validation.ts, zip-guard.ts
│ │ ├── dto/ # Hand-written DTOs (address-label.dto.ts, family.ts)
│ │ └── providers/
│ │ ├── google-places/ # Google Places API client
│ │ └── ministry-platform/ # MP provider
│ │ ├── auth/ # Client-credentials OAuth
│ │ ├── db/ # SQL stored procedures/DDL (bundled into _INSTALL)
│ │ ├── docs/ # Provider documentation
│ │ ├── models/ # Generated types + Zod schemas
│ │ ├── scripts/ # Type/proc generators, SQL bundler
│ │ ├── services/ # 6 API services
│ │ ├── types/, utils/ # Type definitions, HttpClient
│ │ └── client.ts, provider.ts, helper.ts, index.ts
│ │
│ ├── services/ # 9 application services (singletons, server-only)
│ ├── proxy.ts # Route protection (session cookie check + CSP nonce)
│ └── test-setup.ts # Vitest environment setup
│
├── scripts/
│ ├── setup.ts # Interactive setup wizard
│ └── check-prerender.mjs # Fails if a route was statically prerendered (no CSP nonce)
│
├── .claude/ # Claude Code configuration
│ ├── commands/ # 4 Claude Code commands
│ ├── references/ # Agent-facing reference docs (start at INDEX.md)
│ ├── packages/ # Dependency audit records
│ ├── playbooks/ # Porting guides for downstream forks
│ └── TODO/ # Known open issues
│
├── .env.example # Environment template (every variable documented)
├── CLAUDE.md # Development guide
├── vitest.config.mts # Vitest configuration (coverage thresholds)
├── components.json # shadcn/ui config (new-york style, slate theme)
├── next.config.ts # Next.js configuration
├── tsconfig.json # TypeScript strict mode, @/* alias
└── package.json # Dependencies and scripts
The dashboard at / shows a card for each tool. All tools require a Ministry Platform sign-in and an MP security role allowed by MP_SECURITY_ROLES.
| Route | Tool |
|---|---|
/tools/addresslabels |
Address Labels |
/tools/groupwizard |
Group Wizard |
/tools/fieldmanagement |
Field Management |
/tools/addeditfamily |
Add/Edit Family |
/tools/template |
Template (reference implementation) |
Route: /tools/addresslabels
Prints address labels with USPS Intelligent Mail Barcodes (IMb) and optional Word document mail merge.
- Fetch addresses from Ministry Platform selections or the current record
- USPS Intelligent Mail Barcodes (IMb) with configurable Mailer ID, POSTNET fallback
- Print-ready label sheets (Avery 5160 and other stock formats) as PDF
- Word document mail merge with
.docxtemplate support - Address validation and skip reporting
Components (src/components/address-labels/): AddressLabelsForm, AddressLabelsSummary, LabelDocument, AddressLabel, ImbBarcode, PostnetBarcode, MailMergeTab.
Route: /tools/groupwizard
A six-step create/edit wizard for MP Groups records — identity, organization, meeting, attributes, settings, review — with per-step React Hook Form + Zod validation and contact/group search comboboxes. Opened from a Groups record it edits that group; otherwise it creates one.
Components (src/components/group-wizard/): StepIdentity, StepOrganization, StepMeeting, StepAttributes, StepSettings, StepReview, WizardStepper, WizardNavigation, ContactSearch, GroupSearch.
Route: /tools/fieldmanagement
A drag-and-drop editor for Ministry Platform page field layout — reorder fields, assign groups, and toggle per-field flags (required, hidden, writing-assistant) without touching the MP UI.
- Searchable page picker sourced from
dp_Pages - Drag-and-drop reordering within and across field groups
- Inline editing for label, default value, filter clause, depends-on field, and writing-assistant toggle
- Create new groups on the fly
- Auto-merges table columns not yet present in
dp_Page_Fields
Components (src/components/field-management/): PageSearch, FieldOrderEditor, SortableGroup, SortableFieldItem, NewGroupDialog, useFieldOrderState.
Stored procedures: api_MPNextTools_GetPages, api_MPNextTools_GetPageFields, api_MPNextTools_UpdatePageFieldOrder.
Route: /tools/addeditfamily
A household editor: search for a contact (or open from a record), then edit the household, its address (with Google Places autocomplete when configured) and its members, and save. Assigns a unique envelope number when needed. Its UI and server actions live in the route folder, src/app/(web)/tools/addeditfamily/.
Route: /tools/template
A reference implementation of the tool framework pattern. Use it as a starting point for new tools.
src/app/(web)/tools/template/
├── page.tsx # Server component — requireToolAccess(), then parseToolParams()
├── loading.tsx # Loading state
└── template-tool.tsx # Client component — renders inside ToolContainer
All tools follow a consistent pattern:
- Server component (
page.tsx) callsrequireToolAccess(...)first, then parses URL search params withparseToolParams()and fetches page metadata - Client component renders the UI inside
ToolContainerwith header/footer actions - Server actions (
actions.ts) call service singletons, which gate every MP call withAuthorizationService - Tools receive standard MP parameters:
pageID,s(selection),sc,p,q,v,recordID,recordDescription,addl
Quick start — use the Claude Code command to scaffold a new tool:
/newtool MyToolNameOr manually: copy the template folder, rename files and components, and implement your logic inside ToolContainer.
URL parameter utilities:
parseToolParams(searchParams)(src/lib/tool-params.server.ts) — parses params and fetches page metadataisNewRecord(params)/isEditMode(params)(src/lib/tool-params.ts, client-safe) — create vs. edit mode
MPHelper is the entry point to the MP REST API. Application code calls it from service classes in src/services/, never from components or server actions, and every call is preceded by the authorization gate:
import { MPHelper } from '@/lib/providers/ministry-platform';
import { ContactLogSchema } from '@/lib/providers/ministry-platform/models/ContactLogSchema';
import { AuthorizationService } from '@/services/authorizationService';
import { DomainTimezoneService } from '@/services/domainTimezoneService';
const mp = new MPHelper();
const authz = AuthorizationService.getInstance();
// Read — gated like a write
await authz.requireSecurityRole({ table: 'Contacts', operation: 'read' });
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 — $userId comes from the gate, datetimes from DomainTimezoneService.
// The generated schema accepts MP wall-clock datetimes (`YYYY-MM-DD HH:MM:SS`).
const userId = await authz.requireSecurityRole({ table: 'Contact_Log', operation: 'create' });
await mp.createTableRecords('Contact_Log', [{
Contact_ID: 12345,
Contact_Date: await DomainTimezoneService.getInstance().toMpSqlDatetime(new Date()),
Made_By: userId,
Notes: 'Follow-up call completed'
}], {
schema: ContactLogSchema,
$userId: userId
});
// Partial update with Zod validation
await mp.updateTableRecords('Contact_Log', [{ Contact_Log_ID: 67890, Notes: 'Updated' }], {
schema: ContactLogSchema,
partial: true,
$userId: userId
});
// 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 | CRUD operations | getTableRecords, createTableRecords, updateTableRecords, deleteTableRecords |
| Procedure | Stored procedures | getProcedures, executeProcedure, executeProcedureWithBody |
| Communication | Email/SMS with attachments | createCommunication, sendMessage |
| File | File management | getFilesByRecord, uploadFiles, updateFile, deleteFile, getFileContentByUniqueId, getFileMetadata |
| Metadata | Schema info | getTables, refreshMetadata |
| Domain | Domain config | getDomainInfo, getGlobalFilters |
MPHelper also exposes copyRecord, copyRecordWithSubpages and generateSequence. See the provider docs.
# Generate all types with Zod schemas into models/ (recommended)
npm run mp:generate:models
# Generate for specific tables
npm run mp:generate -- -s "Contact"
# Generate with detailed mode (samples records for better type inference)
npm run mp:generate -- -d --sample-size 10
# See all options
npm run mp:generate -- --helpRunning the generators directly? Pass
--conditions=react-servertotsx(e.g.npx tsx --conditions=react-server src/lib/providers/ministry-platform/scripts/generate-types.ts --help). The MP client modules start withimport 'server-only', which throws under plain Node without that condition. Thenpm run mp:generate*scripts already include it.
CLI Options:
| Flag | Description | Default |
|---|---|---|
-o, --output <dir> |
Output directory | ./generated-types |
-s, --search <term> |
Filter tables by name | (all tables) |
-z, --zod |
Generate Zod schemas | false |
-c, --clean |
Remove old files first | false |
-d, --detailed |
Sample records for type inference | false |
--sample-size <num> |
Records to sample | 5 |
Generated output (for mp:generate:models): one TypeScript interface per MP table (JSDoc, FK annotations, constraints), a matching <Table>Schema.ts Zod schema per table, and a barrel index.ts that re-exports the types — import a schema from its own file, e.g. @/lib/providers/ministry-platform/models/ContactLogSchema.
22 shadcn/ui components built on Radix UI primitives and Tailwind CSS v4 (new-york style, slate theme, lucide icons).
- AuthWrapper (
layout/) — server component; validates the session viaauth.api.getSession(), redirects to/signinif unauthenticated and to/session-errorif the session has nouserGuid - ToolContainer (
tool/) — full-screen layout with header, scrollable content, and footer actions. RendersDevPanelabove the title bar on localhost in development only. - ToolHeader — dark slate title bar with optional info tooltip
- ToolFooter — Close/Save action buttons with loading state
- address-labels/ — label printing, IMb/POSTNET barcodes, PDF/DOCX output, mail merge
- group-wizard/ — six-step group create/edit wizard
- field-management/ — drag-and-drop MP page field order editor
- dev-panel/ — localhost-only developer overlay: parsed URL params, MP selection data, contact records, authorized tools, deploy tool
- shared-actions/ — cross-feature server actions:
getCurrentUserProfile()(the calling user's profile; theUser_GUIDcomes from the session, never a parameter), domain config, andhandleSignOut()(app sign-out plus MP RP-initiated logout, used by the sign-out forms on/no-accessand/session-error)
All components use kebab-case file names, PascalCase component names, and named exports with barrel index.ts files.
Application services in src/services/ are server-only singletons that wrap MPHelper:
| Service | Purpose | Key Methods |
|---|---|---|
| AuthorizationService | Role gate for all MP data access (MP_SECURITY_ROLES, fails closed); returns the acting MP User_ID for write attribution |
requireSecurityRole, hasSecurityRole |
| DomainTimezoneService | MP domain time zone and wall-clock ↔ instant conversion at the MP boundary | getMpTimezone, toMpSqlDatetime, parseMpDatetime |
| ToolService | Page metadata, selections, the user's authorized tools, dev-tool deploy | getPageData, getSelectionRecordIds, getUserTools, resolveContactIds, listPages, listRoles, deployTool |
| UserService | Signed-in user's display profile by User_GUID |
getUserProfile |
| AddressLabelService | Mailing addresses for contacts | getAddressesForContacts, getAddressForContact |
| GroupService | Group Wizard lookups, search, read/create/update | fetchAllLookups, searchContacts, searchGroups, getGroup, createGroup, updateGroup |
| FieldManagementService | MP pages, page fields, table metadata, field-order saves | getPages, getPageFields, getTableMetadata, updatePageFieldOrder |
| FamilyService | Add/Edit Family: contact search, household load/save, lookups, envelope numbers | searchContacts, resolveContactIdFromPage, getHousehold, getLookups, getDefaults, getNextEnvelopeNumber, saveHousehold |
| GooglePlacesService | Address autocomplete (key from MP config, else env) | isEnabled, autocomplete, getPlaceDetails |
DTOs (src/lib/dto/): hand-written application types — address labels (LabelData, LabelConfig, SkipRecord, …) and family (Household and related schemas).
- Framework: Vitest 5 with jsdom
- Libraries:
@testing-library/react,@testing-library/user-event,@testing-library/jest-dom - Coverage: v8 provider, enforced thresholds (statements 97%, lines 98%) in
vitest.config.mts - Co-location:
foo.ts→foo.test.ts, next to the source - Counts: test totals drift quickly — trust
npm run test:run; the latest snapshot lives in.claude/references/_meta/facts/
npm test # Watch mode
npm run test:run # Single run
npm run test:coverage # With coverage report (what CI runs)Per-file coverage and the full test inventory are in the Testing Reference.
Key testing patterns:
vi.hoisted()for mock variables referenced insidevi.mock()factories- MPHelper mock: use mock class syntax, not
vi.fn().mockImplementation() - Singleton reset:
(ServiceClass as any).instance = undefinedinbeforeEach - Server action mocks: mock
@/lib/auth,next/headers, and service singletons - Fake timers:
vi.useFakeTimers()for token expiry testing
# Development
npm run dev # Start dev server (Turbopack)
npm run build # Bundle SQL install script, then production build (with type checking)
npm start # Start production server
npm run check:prerender # After a build: fail if a route was statically prerendered (no CSP nonce)
# Code Quality
npm run lint # ESLint (native flat config)
npm run typecheck # tsc --noEmit
npm test # Vitest watch mode
npm run test:run # Single test run
npm run test:coverage # Coverage report
# Ministry Platform
npm run mp:generate:models # Generate types + Zod schemas to models/ (recommended)
npm run mp:generate # Type generator with your own flags (-- --help)
npm run mp:generate:storedprocs # Generate stored procedure reference
npm run mp:build:install # Bundle db/ SQL into _INSTALL/ministryplatform-install.sql
# Setup
npm run setup # Interactive setup wizard
npm run setup:check # Validate setup without changesCI (.github/workflows/test.yml) runs lint, typecheck, tests with coverage, the build, and the prerender check as steps of a single required test job.
dev is the default branch: branch from it and open PRs against it (squash-merged). main is production and only receives dev through a release PR (merge commit, never squash). See CLAUDE.md § Branching & Release.
| Package | Version | Purpose |
|---|---|---|
| next | ^16.4.0 | App Router, Turbopack, React Server Components |
| react | ^19.3.0 | UI framework |
| better-auth | ^1.7.7 | OAuth authentication with genericOAuth plugin |
| jose | ^6.2.12 | id_token (JWT) verification |
| zod | ^4.6.5 | Runtime validation (v4 API) |
| react-hook-form | ^7.89.0 | Form state management |
| tailwindcss | ^4.2.0 | Utility-first CSS |
| lucide-react | ^1.55.0 | Icon library |
| @react-pdf/renderer | ^4.9.0 | Label sheet PDFs |
| docx / docxtemplater | ^9.9.0 / ^3.71.0 | Word document output and mail merge |
| @dnd-kit/react | ^0.5.0 | Drag and drop (Field Management) |
| vitest | ^5.0.0 | Test framework |
| typescript | ^6.0.3 | Type safety |
Dependency audits and known version holds are recorded in .claude/packages/.
This project includes 4 custom Claude Code commands in .claude/commands/:
| Command | Description |
|---|---|
/newtool <name>[, <name>…] |
Scaffold one or more tools: a self-gating route page, a client component in ToolContainer, and a dashboard card |
/newstoredproc <name> |
Scaffold an api_* stored procedure (with @DomainID INT as the first parameter) and add it to the SQL install bundle |
/release |
Promote dev → main with a merge commit, then tag main with calver vYYYY.MM.DD.HHmm and publish a GitHub release (--draft, --prerelease, --tag, --target, --since) |
/update-deps |
Apply in-range updates, evaluate each major separately, sweep OSV.dev for advisories, and write a record to .claude/packages/ (--security-only, --check) |
| Document | Description |
|---|---|
| CLAUDE.md | Development guide — commands, architecture, rules, testing patterns |
| Security Policy | How to report a vulnerability; required steps for downstream forks |
| Reference Index | Entry point to all agent-facing reference docs |
| Ministry Platform Provider | REST API client documentation |
| Type Generator | CLI for generating TypeScript types from the MP schema |
| Auth Reference | Better Auth config, OAuth flow, sessions, userGuid vs user.id |
| Security Reference | Authorization model, write attribution, logging rules, headers/CSP |
| Components Reference | Component inventory |
| Services Reference | Service layer, MP query patterns |
| Testing Reference | Vitest patterns, mock setup, coverage data |
| Decisions / Gotchas | Architectural decisions and known traps |
| MP Schema Reference | Generated table and stored procedure catalogs |
See CLAUDE.md for the full rules. The essentials:
@/*path alias for all internal imports- Naming: PascalCase for components/types, camelCase for functions/variables, kebab-case for all files and folders, snake_case for Ministry Platform API fields
- React Server Components by default;
"use client"only when needed - Named exports only (no default exports, except where Next.js requires them)
- Feature folders in kebab-case with
index.tsbarrels; server actions co-located asactions.ts; shared actions insrc/components/shared-actions/ - Server modules start with
import 'server-only' - Authorize, don't just authenticate: MP-touching actions and service methods call
AuthorizationService.requireSecurityRole(), reads included - Use
session.user.userGuidfor MP lookups (notsession.user.id) - Convert datetimes at the MP boundary with
DomainTimezoneService - Never edit generated files in
src/lib/providers/ministry-platform/models/— regenerate withnpm run mp:generate:models - Access MP fields with special characters using bracket notation:
event["Allow_Check-in"]
Please do not open a public issue for a security problem. Report it privately as described in .github/SECURITY.md, which also lists the steps every downstream fork must take.
No license has been chosen for this repository yet. Until one is added, all rights are reserved by default. ("private": true in package.json only prevents publishing to npm.)
For Ministry Platform API documentation, see your instance's API documentation portal. For bugs and feature requests, open a GitHub issue.