Admin - /api/admin: products CRUD, the enquiry inbox, and presigned photo upload straight to the bucket so images never pass through the app. Gated by platform.security.authenticated-paths = /api/admin/**, so any signed-in Authentik user is staff — the alternative is a role model a two-person bakery would never maintain. - /api/me is deliberately PUBLIC. The SPA asks on every page load, and requiring a login would bounce every anonymous visitor to Authentik just to read the menu. - /admin screens: product list with edit and remove, an editor with drag-free photo reordering and upload, and an enquiry inbox that flags anything the relay refused. Gallery - swipe on touch devices, which the react-awesome-slider it replaced had and this did not, plus arrow keys and position dots — with swipe there is otherwise nothing to say a card holds more than one photo. Vertical drags are ignored so page scrolling still works. - @BatchSize on the photo collection: the products page loaded the whole catalogue and Hibernate issued a query per product for its images, forty-odd round trips for a page that needs two. Three things the tests caught, none of which are obvious: - Adding the storage starter broke every existing test. It activates on a default endpoint, so an S3 client is built even in tests and dies on blank keys. - MockMvc's webAppContextSetup leaves the security filter chain OUT, so the first version of the security test passed 200s and proved the opposite of what it claimed. It needs .apply(springSecurity()). - Turning on the security starter turns on CSRF — for the PUBLIC contact form too, which then 403s. The SPA now reads the XSRF-TOKEN cookie and sends X-XSRF-TOKEN, and there is a test asserting the form is rejected without it. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01XXKjx7FNyRVAjU8dgB5KhN
The Vine Coffeehouse + Bakery — itsthevine.com
Site for The Vine, 215 E Main Street, Princeville, Illinois. Spring Boot serving a Vite/React SPA, on the Bennett platform.
Previously a Next.js app on Cloudflare, then self-hosted; the look is unchanged.
Shape
| Backend | Spring Boot 4 / Java 25, com.itsthevine.web |
| Frontend | Vite + React 19 + TypeScript + Tailwind v4, served from the jar |
| Database | Postgres (itsthevine on the shared app-db cluster), Flyway |
| Photos | public MinIO bucket itsthevine — not in the repo or the image |
| Deploy | Gitea CI → image → Watchtower → Caddy |
What the server owns
The SPA renders; it doesn't decide anything.
/api/products,/api/categories— the catalogue, its curated order, the category filter and the absolute image URLs. This was a TypeScript array shipped to every visitor; it's now a table (V2__products.sql) read throughProductCatalog./api/contact— validates, records the enquiry, emails it, then fans out to the n8n hub. Recorded before sending on purpose: a relay outage costs a notification, not the enquiry. Undelivered ones areenquiry.delivered = false. Validation and delivery come fromplatform-starter-contact, shared with the other sites.- Per-page metadata —
PageMetaControllerrewrites<title>/<meta>/OG tags per route. Next used to server-render these; a plain SPA would hand crawlers and link-preview scrapers one generic shell.
Photos
Re-encoded to webp and uploaded to the bucket once (50 MB of originals → 14 MB), served with a
year-long cache. site.assets.base-url says where they live. The originals remain in this repo's
history. EXIF (including GPS from phone photos) is stripped by the re-encode.
Local development
# backend (needs Postgres on :5432 with an itsthevine database)
mvn spring-boot:run
# frontend, proxies /api to :8080
cd frontend && npm install && npm run dev # http://localhost:2024
Build without the SPA for quick backend loops: mvn -DskipFrontend=true package.
Tests need Docker (Testcontainers):
mvn verify
Configuration
| Variable | Purpose |
|---|---|
DB_URL / DB_USER / DB_PASSWORD |
Postgres |
SMTP_SERVER / SMTP_PORT / SMTP_USERNAME / SMTP_TOKEN |
relay for the contact form |
CONTACT_TO / CONTACT_FROM |
enquiry recipient and envelope sender |
CONTACT_HUB_URL |
optional n8n webhook; best-effort, never blocks a submission |
SITE_BASE_URL |
absolute base for og:url |
VITE_ASSET_BASE / site.assets.base-url |
photo bucket |
A missing CONTACT_TO stops the app from starting. That is deliberate: application.yaml maps it
to platform.contact.to, and an unset variable leaves the property present-but-empty, which is enough
to activate the contact starter. Without the @NotBlank check in platform-starter-contact the site
would come up, show a working contact form, and mail every enquiry to nobody. Better to fail on deploy
than to lose a week of orders.