Personal website and application for Christopher Bell. The app is a Spring Boot monolith with server-rendered Thymeleaf pages and browser-native JavaScript served from Spring static resources.
This README is the root-level technical guide. A beginner should be able to use it to run the app, understand the package layout, and know where to make common changes.
- Java 21
- Spring Boot 3.4
- Gradle Wrapper
- MongoDB
- Thymeleaf templates
- Vanilla JavaScript ES modules
- Bootstrap styles and local CSS
There is no npm build. There is no package.json. Do not run npm install for
this app.
.
|-- build.gradle.kts # Root Gradle build configuration
|-- settings.gradle.kts # Includes website and cbell-lib modules
|-- cbell-lib/ # Shared Java library code
`-- website/ # Spring Boot application
|-- src/main/java/dev/christopherbell/
| |-- account/ # Signup, login, profiles, password reset
| |-- admin/ # Back Office activity and admin views
| |-- blog/ # Config-backed blog content
| |-- configuration/ # Security, filters, app configuration
| |-- location/ # Imported ZIP coordinate reference data
| |-- message/ # Direct messages
| |-- notification/ # Notifications and mention alerts
| |-- photo/ # Photo gallery data
| |-- post/ # Void posts, replies, likes, feeds
| |-- report/ # Post reporting and moderation
| |-- vehicle/ # VIN decoder and vehicle storage
| |-- view/ # Page route controller
| `-- whatsforlunch/ # WFL restaurant import and suggestions
`-- src/main/resources/
|-- application.yml # Default configuration
|-- application-local.yml # Local profile overrides
|-- static/ # CSS, JS, images
`-- templates/ # Thymeleaf HTML templates
Most feature packages have their own README.md. Read the package README before
changing that feature, and update it when behavior or API contracts change.
AI coding agents should also read AGENTS.md. GitHub Copilot-style agents can
use .github/copilot-instructions.md, which points back to the same workflow.
website- the runnable Spring Boot web application.cbell-lib- reusable Java library code shared by the app.
The root Gradle build sets Java 21 for subprojects and enables JUnit Platform for tests.
- Java 21 JDK
- MongoDB
- A shell that can run the Gradle wrapper
Optional for email/password reset work:
- Resend API key
- Verified sender domain
Spring loads configuration from:
website/src/main/resources/application.ymlwebsite/src/main/resources/application-local.yml- optional
.envfiles from the repo root orwebsite/ - environment variables
The default Spring profile is local.
Important local defaults:
- App port:
8081 - Mongo database:
christopherbell - Mongo URI:
mongodb://localhost:27017
Useful environment variables:
export SPRING_PROFILES_ACTIVE=local
export SPRING_DATA_MONGODB_URI=mongodb://localhost:27017
export RESEND_API_KEY=re_your_resend_key
export APP_MAIL_FROM=noreply@your-verified-domain.com
export APP_JWT_SECRET=replace-with-at-least-32-random-charactersFor local secrets, copy .env.example to .env and fill in values. Do not
commit .env.
Start MongoDB first. Then run commands from the repository root:
./gradlew :website:bootRunOpen:
http://localhost:8081
For Windows PowerShell:
.\gradlew.bat :website:bootRunBuild the application and run tests:
./gradlew :website:buildThe runnable JAR is written under:
website/build/libs/
Run a built JAR:
java -jar website/build/libs/<jar-name>.jarRun the full test suite:
./gradlew testRun only the website tests:
./gradlew :website:testRun one test class:
./gradlew :website:test --tests dev.christopherbell.whatsforlunch.restaurant.RestaurantServiceTestFor quick JavaScript syntax checks, use Node directly. This is only a parser check; it is not an npm workflow.
node --check website/src/main/resources/static/js/back-office.jsBackend code is organized by feature, not by technical layer. For example,
post, account, vehicle, and whatsforlunch each own their controller,
service, repository, mapper, models, and package docs.
Common pattern inside a feature:
*Controllerdefines HTTP endpoints.*Serviceowns business rules.*Repositoryowns MongoDB access.*Mapperconverts between persistence models and API DTOs.model/contains entities, request DTOs, response DTOs, and enums.README.mdexplains the feature's technical behavior.
Cross-cutting web infrastructure lives in configuration. Server-rendered page
routes live in view.
Frontend files are plain browser assets. They are served directly by Spring from:
website/src/main/resources/static
Important folders:
static/js/app.jswires shared page behavior.static/js/components/contains reusable web components.static/js/lib/contains shared API paths, fetch helpers, feed rendering, and utilities.static/js/*.jspage modules add behavior for individual pages.static/css/main.csscontains application styling.
Templates live in:
website/src/main/resources/templates
If you add a new page:
- Add a Thymeleaf template in
templates/. - Add a route in
ViewControllerif needed. - Add a page script in
static/js/if the page needs browser behavior. - Add shared API routes to
static/js/lib/api.js. - Keep public/protected access rules aligned in
SecurityConfig.
Authentication uses JWTs created by PermissionService. JWT signing uses the
stable APP_JWT_SECRET value so tokens remain valid across app restarts and
instances. API protection is configured in SecurityConfig and method-level
@PreAuthorize annotations.
Public routes are listed in SecurityConfig.PUBLIC_URLS. Admin-only behavior
usually uses:
@PreAuthorize("@permissionService.hasAuthority('ADMIN')")When adding a public API endpoint, update both:
SecurityConfig.PUBLIC_URLS- the frontend API path in
static/js/lib/api.js
- Accounts: signup, login, public profiles, follows, password reset.
- Void posts: posts, replies, likes, global feed, following feed, user feeds.
- Messages: user-to-user conversations.
- Notifications: unread counts, read state, mention notifications.
- Reports: users can report posts; admins resolve reports in Back Office.
- Back Office: admin queues and operations.
- Vehicles: VIN decoding, stored vehicles, NHTSA enrichment.
- What's For Lunch: multi-metro restaurant import and nearby lunch suggestions.
- Photos and blog: content configured through application properties.
WFL restaurant data is imported from OpenStreetMap through Overpass. The import is admin-only and can be triggered from Back Office. The default import coverage includes Austin, the San Francisco Bay Area, New Orleans, and Dallas.
The public WFL page asks for the browser location or a ZIP code and returns up to three restaurant suggestions within the selected radius. Browser-location searches use the provided coordinates; ZIP searches resolve the radius origin from imported Location Census ZIP Code Tabulation Area coordinates. Both paths still depend on saved restaurant coordinates, so run the import when local data is empty or missing coordinates.
Location owns the reusable Census ZCTA coordinate reference data. Public lookups
use GET /api/location/zip/{zipCode} after an admin imports the bundled Census
dataset. The payload includes source and source year because the coordinates are
Census internal points, not USPS delivery geometry.
Import or refresh ZIP coordinates from Back Office or with:
curl -X POST \
-H "Authorization: Bearer <admin-token>" \
http://localhost:8081/api/location/zip/import/censusAdmin endpoints:
curl -X POST \
-H "Authorization: Bearer <admin-token>" \
http://localhost:8081/api/whatsforlunch/restaurant/2026-05-17/import/openstreetmap
curl -X POST \
-H "Authorization: Bearer <admin-token>" \
http://localhost:8081/api/whatsforlunch/restaurant/2026-05-17/dedupe-namesAdd or change an API endpoint:
- Find the feature package under
website/src/main/java/dev/christopherbell. - Update the controller and service.
- Add or update request/response models under
model/. - Update tests for the controller and service.
- Add the frontend route in
static/js/lib/api.jsif browser code calls it. - Update the feature package README.
Add a field to a Mongo-backed model:
- Update the entity in the feature
model/package. - Update request/detail DTOs if the field is part of the API contract.
- Update MapStruct mapper tests or service/controller tests.
- Consider whether old documents need migration or default handling.
Change page behavior:
- Find the template in
website/src/main/resources/templates. - Find the page script in
website/src/main/resources/static/js. - Keep reusable behavior in
static/js/libonly if multiple pages need it. - Check browser syntax with
node --check.
Build:
./gradlew :website:buildSet production configuration with environment variables:
export SPRING_PROFILES_ACTIVE=prod
export SPRING_DATA_MONGODB_URI=mongodb://<host>:<port>/<db>
export SERVER_PORT=8080
export RESEND_API_KEY=re_your_resend_key
export APP_MAIL_FROM=noreply@your-verified-domain.comRun:
java -jar website/build/libs/<jar-name>.jarGradle wrapper has Windows line endings in WSL:
sed -i 's/\r$//' gradlew
chmod +x gradlewCannot connect to MongoDB:
- Confirm MongoDB is running.
- Confirm the URI matches the active profile.
- Override with
SPRING_DATA_MONGODB_URIif needed.
Static JS changes are not visible:
- Hard-refresh the browser.
- Restart
:website:bootRunif template or server config changed.


