The Kept Journal
A travel journal site with a built-in admin and CMS, running on Cloudflare Workers with a Supabase Postgres database.
- Period
- Aug 2026 – present
- Role
- Sole designer and engineer: product, full-stack TypeScript, database design, and deployment.
Problem
I wanted a travel journal that publishes trip entries together with their real costs, and a back office to run it from one place. Entries, trips, photos, the cost ledger, comments and the publishing pipeline all live in one system.
Hosting had to be cheap and simple for one person to run, and production had to be a build artefact that is never edited by hand on the server.
What I built
- A public site on Next.js 16 (App Router) and React 19 in strict TypeScript: journal list, entry pages with their cost ledger, trip pages, tools, an about page, legal pages and a sitemap.
- An admin CMS under /admin with more than 20 pages: entries, a publishing pipeline, trips (including import), a finance ledger, comments with weekly digests, partners with a media kit and rate card, a plan board, users, feedback, settings and connectors.
- A relational data model in Prisma 6 with 39 models and 30 enums, covering entries, trips, a categorised ledger, posts with a status lifecycle, and comments with AI classifications and digests.
- Single-owner admin authentication with an HMAC-SHA256-signed, httpOnly session cookie compared in constant time. A proxy guards /admin, and every mutating server action also checks the session itself.
- An encrypted credential store for third-party API keys, entered from the admin instead of environment files: AES-256-GCM with a random IV per write and an HKDF-SHA256 derived key. The UI shows only a masked hint.
- AI comment triage and digests with the Anthropic SDK, using zod-typed structured outputs and a cached static system prompt. With no key it runs in a deterministic mock mode, so the whole app works locally without credentials.
- A publishing layer behind a pluggable interface: a direct publisher, rented scheduling services, or a deterministic mock. Each send's Post row is the idempotency key, so re-running a schedule creates no duplicates.
- An image upload pipeline that detects file type from magic bytes instead of trusting the client, reads size and EXIF in pure JavaScript, and writes through a swappable local or S3-compatible storage driver.
- A Cloudflare Workers deployment built with the OpenNext adapter. A thin Worker entry re-exports the generated handler and adds scheduled jobs, including a database keep-alive and daily sync jobs authenticated with a derived token.
Engineering highlights
One schema, two databases
Prisma needs a literal datasource provider, so a small script rewrites that one line before generate. Local work uses SQLite with db push; production uses Postgres with reviewed migrations.
Postgres from Workers via Hyperdrive
The Prisma pg driver adapter reads the connection string from the Worker binding and falls back to DATABASE_URL only outside Cloudflare, for example in local scripts.
A deliberate caching choice
Every page that reads the database renders on demand, and only a few pages are prerendered with no runtime revalidation. A read-only static-assets incremental cache is therefore enough: no R2 bucket or KV namespace to run, and no cache-write burst on deploy. The config documents the exact change that would require moving to R2.
Defence in depth on the admin
The proxy is not the only lock: each mutating server action re-checks the session on its first line, following Next.js guidance to treat server actions as public endpoints.
Row Level Security as a design guard
A read-only script queries the live Postgres catalogue and exits non-zero if any public table lacks RLS, so a future migration that forgets it is caught.
Cheap, dependency-free checks
Correctness checks are small tsx scripts that exit non-zero, wired into package.json. They cover data and pricing logic, a scan of client and server bundles for leaked server-only code, RLS, and auth.
Codebase at a glance
- 39 Prisma models and 30 enums
- 12 Prisma migrations
- 24 admin page components
Stack
- Next.js 16
- React 19
- TypeScript
- Tailwind CSS v4
- Prisma 6
- PostgreSQL (Supabase)
- SQLite (local dev)
- Cloudflare Workers
- OpenNext for Cloudflare
- Cloudflare Hyperdrive
- Anthropic SDK
- zod
- Playwright
- pnpm