- TypeScript 92%
- CSS 5.6%
- Just 2.1%
- Dockerfile 0.1%
README.md was still the Fresh scaffold boilerplate. Rewritten as an actual entry point: build badge, quick start, the four-env-file table, a `just` command table, testing setup, project layout, the four architecture constraints that are expensive to unwind, and a doc map pointing at AGENTS.md, src/AGENTS.md, wiki/, LEARN.md, and DEPLOY.md — all of which were previously invisible from the repo root. TESTING.md was a raw ChatGPT transcript from April 2026, complete with `## Prompt:` / `## Response:` headers. Its proposed `scripts/ci-*.sh` were never built; CI is Forgejo Actions running `just validate`. The real testing conventions live in AGENTS.md and test.just. The README does not claim `just docker::up` works — no docker-compose.yml is committed yet, as DEPLOY.md's own follow-up list notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD9ydGg2JNTri6omU3he4Z |
||
|---|---|---|
| .agents/commands/fjx | ||
| .claude | ||
| .forgejo/workflows | ||
| .githooks | ||
| .vscode | ||
| certs | ||
| coverage | ||
| openspec | ||
| src | ||
| supabase | ||
| wiki | ||
| .dockerignore | ||
| .env.test.example | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .yamllint | ||
| admin.just | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| deno.json | ||
| deno.lock | ||
| docker.just | ||
| Dockerfile | ||
| env.example | ||
| fjx.json | ||
| justfile | ||
| LEARN.md | ||
| migrate.just | ||
| README.md | ||
| supabase.just | ||
| test.just | ||
trippin
Personal travel logging app — trips, points of interest, photos, and a map-first browsing surface. A migration of the old FotoLog.
Fresh 2 (Deno) + Preact islands · Supabase (Postgres, Auth, Storage) · Lit for the Mapbox layer · Tailwind CSS 4 · an in-process background worker.
Quick Start
just setup # check tools, install git hooks, cache deps, create .env
supabase start # local Supabase stack (Docker)
just test::env # generate .env.test = local Supabase creds + your .env
just dev # http://localhost:5173
just setup verifies you have deno, docker, just, and supabase on PATH and tells you where to get whatever is missing.
Migrations run automatically at app boot (src/main.ts), so the first just dev brings the schema up. To apply them without starting the app: just migrate up.
Grant yourself admin: just admin::grant you@example.com.
Re-run
just test::envafter everysupabase start(keys rotate) and after every edit to.env.just devreads.env.test, not.env.
Environment
| File | Role |
|---|---|
env.example |
template — every supported variable, documented |
.env |
the file you edit; dev-safe values only. Copied wholesale into .env.test |
.env.test |
generated by just test::env — local Supabase credentials + everything from .env. Never hand-edit |
.env.prod |
manually maintained, points at live infra. Nothing reads it unless you ask: just migrate up .env.prod, ENV_FILE=.env.prod just psql |
The app runs without the optional keys — Mapbox, OpenRouter (vision + embeddings), Postmark/SMTP, and Turnstile each degrade rather than crash. See env.example for what each one switches on.
Common Commands
Everything goes through just. Run just with no arguments for the full list including submodules.
| Command | What it does |
|---|---|
just dev |
Vite dev server (ensures the Supabase stack is up first) |
just build |
production build — Vite + the Lit component bundle |
just components |
rebuild src/static/components.js only (after editing src/lib/web-components/) |
just icons |
regenerate the inline icon registry from src/assets/icons/ |
just check |
fast gate: format, lint, type-check |
just validate |
what CI runs: check + coverage ratchet + audit + secrets + SAST + fuzz |
just test::all |
full test suite |
just test::units |
unit/integration only (excludes src/e2e/ and src/fuzz/) |
just test::e2e |
end-to-end only |
just test::coverage |
tests with coverage; refreshes .coverage/summary.json |
just psql |
psql shell against the same DB just dev uses |
just migrate up [env] |
apply pending SQL migrations (defaults to .env.test) |
just supabase::status |
local stack URLs, keys, running services |
just supabase::verify .env.prod |
preflight a Supabase project against what the app needs |
just admin::grant <email> |
grant the site-admin role |
just release <version> |
bump version, commit, tag, push |
Testing
Tests run against real Supabase — no mocking. Unit and integration tests live next to their source (foo.ts → foo.test.ts); end-to-end tests live in src/e2e/.
supabase start && just test::env # once per stack restart
just test::all
Two isolation helpers, both from src/lib/testing/mod.ts:
createTestUser()— a real, pre-confirmed Supabase Auth user plus acleanup()you must always call in afinally.withTransaction()— wraps the body in a transaction that is always rolled back. Only valid when the code under test uses the singlePoolClientyou hand it; code that opens its own pool connections (e.g.processEmailBatch) will not see the uncommitted rows.
The dev stack and the test suite share one database. Three rules keep a test run from spilling into your just dev session: fixtures only ever address @test.invalid / @example.com (the worker refuses to dial reserved domains), just test::_tested purges accounts created during the run, and the middleware treats an auth 5xx as a transient outage rather than a logout. Local GoTrue mail lands in Inbucket at http://localhost:33304.
Full conventions, including how to test Fresh 2 route handlers and middleware: AGENTS.md.
Layout
src/
main.ts # entry — env validation, migrations, worker start, Fresh app
routes/ # file-system routes (pages + /api)
islands/ # interactive Preact islands
components/ # server-rendered Preact components
lib/
supabase.ts # server-side Supabase client (SSR cookie auth)
db/ # Postgres pool + migration runner
worker/ # queue, registry, task/email batches, loop
curation/ # photo curation lifecycle — the largest subsystem
pois/ trips/ teams/ sharing/ auth/ admin/ email/ globe/
web-components/ # Lit — Mapbox-coupled elements only
testing/ # test factories and helpers
migrations/ # numbered SQL, applied at boot
e2e/ # end-to-end tests
static/ # served as-is, incl. the built components.js
Root holds config only (deno.json, justfile + *.just modules, Dockerfile).
Architecture Notes
Four constraints that are easy to violate and expensive to unwind:
- Queue writes go through
src/lib/worker/queue.ts. NeverINSERTintotasksoremailsdirectly — the queue is where dedup keys are derived, and that's what makes enqueues safe to retry. - Lit is for Mapbox only.
trip-map,globe-view, map controls, plustrip-lightboxandtags-input. Every other piece of UI is Preact so it gets SSR, typed props, and one build path. Cross-framework signalling uses document-level CustomEvents. - Map UI lives inside the map. Don't absolutely-position controls over a map component from outside; implement
MapControland register it. - Storage URLs are signed. Trip media is private and served through short-lived signed URLs generated server-side. Avatars are the one exception — their own public-read bucket.
Where the Docs Live
| Doc | What it holds |
|---|---|
| AGENTS.md | the operational "how" — dev commands, testing conventions, key constraints. Symlinked as CLAUDE.md |
| src/AGENTS.md | frontend rules — component policy, Button pattern, theme tokens, form/drawer layout, browser-debugging order |
| wiki/README.md | the "why" — design stances, architecture rationale, and debugging post-mortems. Start here for background on a decision |
| wiki/GLOSSARY.md | canonical domain vocabulary. Use these exact terms in code, prompts, and docs |
| LEARN.md | the five load-bearing files, in reading order, for getting oriented fast |
| DEPLOY.md | production: Supabase Cloud provisioning, prod .env, image build, VPS run, verification, rollback |
openspec/ |
spec-driven change proposals and living capability specs — managed by the /opsx:* tooling, not hand-edited |
Deployment
Single Docker image, one container, behind Caddy on a VPS; Supabase Cloud for Postgres, Auth, and Storage. The background worker runs in the same process as the web app, which is why exactly one container may run at a time.
just docker::build # docker build -t trippin .
just docker::up expects a docker-compose.yml that is not committed yet — DEPLOY.md carries the compose and Caddy config to paste onto the host.
Full production procedure — provisioning, TLS-to-Postgres gotchas, the prod environment file, first-boot verification, and rollback — is in DEPLOY.md.
CI is Forgejo Actions (.forgejo/workflows/build.yaml); it runs just validate against a warm Supabase stack on the runner, serialized repo-wide.