This document is the current developer and maintainer reference for the Spokes web application. For a non-technical owner handoff guide, see owner-user-guide.md.
The application is a Next.js app with two public boards:
- Jobs at
/jobs - Events at
/events
Authenticated nonprofit users can submit and manage their own listings. Spokes admins can approve or reject submissions, manage users, and delete users or organizations.
- Framework: Next.js App Router
- UI: React, Tailwind CSS, Chakra UI
- Authentication: Clerk
- Database: MongoDB through Mongoose
- Email: Resend
- Tests: Jest
- Package scripts:
npm run lint,npm test,npm run build
src/app: Next.js routes and API route handlerssrc/components/jobs: Job board, job form, job cards, and job admin UIsrc/components/events: Event board, event form, event cards, and event admin UIsrc/components/users: Admin user-management UIsrc/components/Onboarding: First-time profile setupsrc/database: Mongoose schemas and database connectionsrc/lib: Shared auth, validation, links, organization, URL, and formatting helperssrc/types: Shared TypeScript typesdocs/owner-user-guide.md: Non-technical owner guide
/jobs: public job board/events: public event board/jobs/list: create or edit a job/events/list: create or edit an event/jobs/manage: nonprofit job dashboard/events/manage: nonprofit event dashboard/events/[eventId]: public event detail page/onboarding: first-time user profile setup/sign-in,/sign-up,/jobsLogin,/eventsLogin: Clerk sign-in/sign-up flows
/jobs/admin: admin job review dashboard/events/admin: admin event review dashboard/jobs/users: admin user-management page, with a back link to jobs admin/events/users: admin user-management page, with a back link to events admin/admin/users: standalone admin user page
src/app/not-found.tsx renders the 404 page. It is intentionally board-neutral and offers links to both /jobs and /events. While the 404 page is mounted, the global navbar hides board-specific navigation and login/user controls.
Auth helpers live in src/lib/auth.ts.
Roles are derived from Clerk auth state:
job_seeker: unauthenticated visitornonprofit: authenticated user not in the Spokes admin organizationspokes_admin: authenticated user whose Clerk organization slug isspokes-admin
Most API routes use withApiAuth, which accepts:
requireAuth: true | falseallowedRoles: [...]
Admin-only UI pages also check the role server-side before rendering.
src/middleware.ts handles onboarding redirects and some route protection. It still contains legacy route matcher names such as jobsDashboard and eventsDashboard. The current application routes live under /jobs and /events, so review middleware carefully before changing route protection or adding new protected sections.
Files:
src/app/onboarding/page.tsxsrc/components/Onboarding/OnboardingForm.tsxsrc/app/api/users/route.ts
First-time authenticated users must complete onboarding before normal app use. Onboarding collects:
- Organization name
- Paid member status
The "I am unsure if I am a paid member" option submits paidMember: false. Admins can later update the member status from the user-management page.
When onboarding succeeds:
- A MongoDB user record is created or updated.
- Clerk public metadata is updated with
onboardingComplete: true.
File: src/database/userSchema.ts
Fields include:
_id: Clerk user IDnameemailpostedJobspostedEventspaidMemberorganizationName
User records are the bridge between Clerk identity and MongoDB-owned app data.
File: src/database/jobSchema.ts
Core fields:
organizationNameuserIdorganizationIndustrytitlepostDatemodifiedDateapprovedDatejobDescriptionemploymentType:part-time,full-time, orvolunteercompensationType:salary,hourly, orcontractjobStatus:pending,approved,rejected, orexpireddetailURLapplyNowURLrejectionMessagememberJob
The memberJob field is copied from the user's paidMember status when a job is created. Updating a user's member status also updates that user's existing jobs.
File: src/database/eventSchema.ts
Core fields:
dateeventNametimelocationlocationLinkeventLocationGeneraleventLocationGeneralOthereventLocationCityeventLocationCityOtherdescriptionmajorFundraisingEventeventLinkorganizationpublicContactEmailpublicContactPhoneNumbersubmitterFirstNamesubmitterLastNamesubmitterEmailsubmitterPhoneNumbercreatedByUserIdeventStatus:pending,approved,rejected, orexpiredapprovedDaterejectionMessage
Events do not have a location type. There is no remote/in-person field in the current schema, form, API validation, or display cards.
Event region and city options live in src/lib/eventOptions.ts.
GET /api/jobs
- Public.
- Supports filters for
employmentType,compensationType,organizationIndustry, andjobStatus. - Non-admin requests are paginated with
pageandlimit. - Admin requests use
admin=trueand return all matching records. - Approved non-admin results exclude old expired approved jobs by approved date.
- Sorting prioritizes
memberJobfirst, then approved date or post date.
POST /api/jobs
- Requires
nonprofitorspokes_admin. - Creates a pending job.
- Uses the authenticated user's organization unless the requester is a Spokes admin creating for another organization.
- Uses a unique job signature with user, organization, title, post date, and detail URL to avoid duplicate inserts.
- Adds the job ID to the user's
postedJobs.
GET /api/jobs/[jobId]
- Public.
- Fetches one job by ID.
PUT /api/jobs/[jobId]
- Requires
nonprofitorspokes_admin. - Nonprofits can update only their own jobs.
- Nonprofit edits send the job back to
pending. - Supports renewal and unpublish actions.
- Admin status changes can approve, reject, or expire jobs.
DELETE /api/jobs/[jobId]
- Requires
nonprofitorspokes_admin. - Nonprofits can delete only their own jobs.
- Deletes the job document.
POST /api/jobs/recent
- Public.
- Accepts a list of job IDs from browser storage and returns matching jobs.
GET /api/userjobs?userId=...
- Requires
nonprofitorspokes_admin. - Nonprofits can fetch only their own jobs.
- Reads job IDs from the user's
postedJobslist.
GET /api/events
- Public.
- Supports
eventStatus. - Non-admin requests default to approved events only.
- Admin requests use
admin=true. - Sorts approved events by approved date and other statuses by creation date.
POST /api/events
- Requires
nonprofitorspokes_admin. - Validates required event fields in
src/lib/events.ts. - Creates or updates a pending event using a unique event signature.
- Uses the authenticated user's organization unless the requester is a Spokes admin creating for another organization.
- Sends admin notification email from the client after successful creation.
GET /api/events/[eventId]
- Public.
- Fetches one event by ID.
PUT /api/events/[eventId]
- Requires
nonprofitorspokes_admin. - Nonprofits can update only their own pending events.
- Spokes admins can update event status to approved, rejected, pending, or expired.
- Rejected events store a rejection message.
DELETE /api/events/[eventId]
- Requires
nonprofitorspokes_admin. - Nonprofits can delete only their own events.
- Deletes the event document.
- There is currently no visible owner-facing delete button for a single event.
GET /api/userevents?userId=...
- Requires
nonprofitorspokes_admin. - Nonprofits can fetch only their own events.
- Queries by
createdByUserId, which supports both legacy and current event records.
GET /api/users
- Requires
spokes_admin. - Returns all users for the admin user-management page.
POST /api/users
- Requires authentication.
- Creates or updates the authenticated user's MongoDB profile during onboarding.
- Resolves organization names case-insensitively against existing organizations.
GET /api/users/[id]
- Requires
nonprofitorspokes_admin. - Nonprofits can fetch only their own user record.
PATCH /api/users/[id]
- Requires
spokes_admin. - Supports
paidMemberandorganizationNameupdates. - Updating
paidMemberupdates the user's jobs'memberJob. - Updating
organizationNameupdates the user's jobs and events to the canonical organization name.
DELETE /api/users/[id]
- Requires
spokes_admin. - Admins cannot delete their own user.
- Deletes the user plus their jobs and events.
GET /api/organizations
- Requires
nonprofitorspokes_admin. - Returns distinct organization names from users, jobs, and events.
DELETE /api/organizations
- Requires
spokes_admin. - Admins cannot delete their own organization.
- Deletes all users, jobs, and events matching the organization name.
- This is destructive and cannot be undone from the app.
Organization helpers live in src/lib/organizations.ts.
Email routes use Resend:
POST /api/send/new: new job notificationPOST /api/send/update: updated job notificationPOST /api/send/reject: rejected job notificationPOST /api/send/event-new: new event notificationPOST /api/send/event-reject: rejected event notification
Templates live in src/components/EmailTemplates.
Admin links use src/lib/url.ts, which reads:
NEXT_PUBLIC_ADMIN_URLNEXT_PUBLIC_API_BASE_URL
Files:
src/components/jobs/pages/JobsBoardPage.tsxsrc/components/jobs/JobCardsrc/components/jobs/JobGridsrc/components/ui/FilterCard.tsx
The public job board fetches approved jobs with pagination and filters. Recently viewed jobs are stored in browser storage and fetched through /api/jobs/recent.
Files:
src/components/jobs/pages/ListJobPage.tsxsrc/app/jobform/JobFormPage.client.tsx
The job form supports create and edit modes. Existing jobs are edited with a jobId query parameter. Nonprofit edits return jobs to pending review.
File: src/components/jobs/pages/JobsManagePage.tsx
Shows the authenticated user's jobs. Organization users can:
- Edit jobs
- View rejection feedback
- Renew expired jobs
- Unpublish approved jobs
File: src/components/jobs/pages/JobsAdminPage.tsx
Shows pending, approved, rejected, and expired jobs. Admins can:
- Approve jobs
- Reject jobs with a reason
- Renew or expire jobs through status actions
- Refresh dashboard data
- Navigate to user management
Files:
src/components/events/pages/EventsBoardPage.tsxsrc/components/events/EventCard.tsxsrc/components/events/EventCard/EventGrid.tsx
The event board shows approved events. Filters are based on event region and city.
File: src/components/events/pages/ListEventPage.tsx
The event form supports create and edit modes. Existing events are edited with an eventId query parameter. Required fields are validated by src/lib/events.ts.
The form requires confirmation that the event is one of the organization's major fundraising events of the year.
File: src/components/events/pages/ManageEventsPage.tsx
Shows the authenticated user's events grouped into live and pending sections. Rejected events show feedback and can be edited.
File: src/components/events/pages/AdminEventsPage.tsx
Shows pending, approved, and rejected events. Admins can:
- Approve events
- Reject events with a reason
- Refresh dashboard data
- Navigate to user management
File: src/components/users/AdminUsersPage.tsx
Admins can:
- Search users by name, email, or organization
- Filter users by member status
- Export users as CSV or JSON
- Toggle member status
- Edit a user's organization
- Delete a user and their jobs/events
- Delete an organization and all related users/jobs/events
Files:
src/components/NavBar/NavBar.tsxsrc/components/NavBar/TopSection.tsxsrc/components/NavBar/BottomSection.tsx
The top section shows the Spokes logo plus Clerk login/user controls. The bottom section changes labels and links based on whether the current path is under /events or /jobs.
On the 404 page, the navbar hides board-specific links and user/login controls.
Expected variables include:
MONGO_URI: MongoDB connection stringNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: Clerk publishable keyCLERK_SECRET_KEY: Clerk secret keyNEXT_PUBLIC_CLERK_SIGN_IN_URL: Clerk sign-in routeRESEND_API_KEY: Resend API keySENDER_EMAIL: email sender addressADMIN_EMAIL: admin recipient for notification emailsNEXT_PUBLIC_ADMIN_URL: admin URL used in emailsNEXT_PUBLIC_API_BASE_URL: optional base URL used by some frontend API calls
Never commit production secrets. Rotate keys if a secret is accidentally exposed.
Useful commands:
npm run lint
npm test
npm run buildTargeted API tests live in src/app/api/__test__.
Recent areas with tests:
- Job API behavior
- Event API behavior
- Event validation
- User API behavior
- Organization API behavior
- Job board local storage behavior
middleware.tshas legacy route matcher names. Review it before relying on middleware for current/jobs/*and/events/*protection.- Single-event deletion is supported by the API but not exposed as a visible owner-facing button.
- Deleting a user also deletes that user's jobs and events.
- Deleting an organization deletes all matching users, jobs, and events.
- Event records no longer use
locationType; old database documents may still contain that property, but the current app does not read or write it. - Job and event email delivery depends on Resend configuration.
- The admin user page mutates real data. Be careful with destructive actions while testing.
Update this document when changing:
- Route names or page responsibilities
- API request/response behavior
- Data schemas
- Auth or role rules
- Admin workflows
- Email templates or email routing
- Environment variables