A personal trip-journal app: log trips and notes, and get instant, offline-capable
full-text search across every entry — powered by a build-time Pagefind
search index instead of a database LIKE query or a hosted search service.
Backend: ASP.NET Core 10 minimal API + EF Core 10 + PostgreSQL (Docker Compose). Frontend: React 19 + TypeScript 7 + Vite 8 + Tailwind CSS v4. Search: Pagefind, 100% client-side against a static index rebuilt from the data.
- .NET 10 SDK
- Node.js 22 LTS (or newer, meeting Vite 8's 22.12+ minimum)
- Docker with Docker Compose
cp .env.example .envThe defaults work as-is for local development. .env is git-ignored — never commit it.
docker compose up -dThis starts Postgres 17 in a container with a named volume (tripnotes-db-data), empty.
Schema comes only from EF Core migrations — there's no seed SQL and no manual DDL.
cd backend/TripNotes.Api
dotnet ef database updateIf dotnet ef isn't installed:
dotnet tool install --global dotnet-ef --version 10.0.12
export PATH="$PATH:$HOME/.dotnet/tools" # add to your shell profile to persistcd backend/TripNotes.Api
dotnet run --urls http://localhost:5032Verify it's up: curl http://localhost:5032/api/trips should return [] on a fresh
database. In development mode, the OpenAPI document is at
http://localhost:5032/openapi/v1.json. TripNotes.Api.http
has a full request sequence you can run from VS Code's REST Client / Rider / IntelliJ HTTP client.
In a separate terminal:
cd frontend
npm install
npm run devOpen http://localhost:5173. The dev server proxies /api/* to the backend at
localhost:5032, and the backend's CORS policy also allows that origin directly.
Create a trip, add a couple of notes — this is real data going into your local Postgres, no mock/demo data anywhere.
The backend has no search endpoint — search is 100% client-side against a static index built from your data:
cd frontend
npm run build-search-indexThis fetches /api/export from the running backend, writes one HTML fragment per note,
runs Pagefind over them, and writes the index to frontend/public/pagefind/ (git-ignored,
regenerated on demand). The dev server serves it immediately — reload the page and search
should return your real notes.
Re-run this manually any time your data changes — it is not automatic. If you add, edit, or delete a trip/note and want it reflected in search, re-run the command above.
cd backend
dotnet testThese are integration tests that boot the real API via WebApplicationFactory and hit it
over HTTP — not mocks. They run against a separate tripnotes_test database on the same
Postgres container (auto-created and migrated on first run), so docker compose up -d
must be running first. Tables are truncated between tests; your tripnotes (dev)
database is never touched.
To reset to a completely clean database (e.g. after pulling a new migration, or if data gets into a weird state):
docker compose down -v && docker compose up -d
cd backend/TripNotes.Api && dotnet ef database updatedown -v removes the named volume, so the next up starts Postgres from scratch, and
database update reapplies every migration in order from nothing.
tripnotes/
├── backend/
│ ├── TripNotes.Api/ # ASP.NET Core minimal API
│ │ ├── Models/ # Trip, NoteEntry (EF Core entities)
│ │ ├── Data/ # DbContext, migrations, design-time factory
│ │ ├── Dtos/ # request/response records
│ │ ├── Endpoints/ # route group extension methods
│ │ └── TripNotes.Api.http # exercise every route manually
│ ├── TripNotes.Api.Tests/ # MSTest integration tests
│ └── TripNotes.slnx
├── frontend/
│ ├── src/
│ │ ├── api/ # typed fetch client for the backend
│ │ ├── components/ # TripList, TripForm, NoteEditor
│ │ ├── hooks/ # useTrips, useNotes
│ │ └── search/ # SearchBox + pagefind.js loader
│ ├── scripts/build-search-index.mjs
│ └── public/pagefind/ # generated, git-ignored
├── docker-compose.yml # Postgres only
├── .env.example
└── CLAUDE.md # project instructions for AI-assisted development
| Route | Description |
|---|---|
GET /api/trips |
List trips |
POST /api/trips |
Create a trip |
GET /api/trips/{id} |
Get a trip |
PUT /api/trips/{id} |
Update a trip |
DELETE /api/trips/{id} |
Delete a trip (cascades its notes) |
GET /api/trips/{id}/notes |
List notes for a trip |
POST /api/trips/{id}/notes |
Create a note under a trip |
PUT /api/notes/{id} |
Update a note |
DELETE /api/notes/{id} |
Delete a note |
GET /api/export |
Full trips+notes JSON dump — the only thing the search indexer consumes |
There is no /api/search — search is entirely client-side.
No authentication, no cloud deployment/CI, no mobile app, no photo upload, no real-time sync, no tags/categories. See CLAUDE.md for the full rationale — this is a scoped weekend build, not a product.
-
docker compose up -dstarts Postgres cleanly;dotnet ef database updateapplies migrations to a fresh volume with no manual steps. -
dotnet runstarts the API; all CRUD routes work via the.httpfile. -
npm run devstarts the frontend; trips and notes can be created/edited/deleted against the real API. -
node scripts/build-search-index.mjsproduces a working index; typing in the search box returns real matches from actually-typed note content. -
dotnet testpasses. - This README documents the full setup from a clean clone.