- TypeScript 93.5%
- CSS 4.5%
- Just 1.8%
- Dockerfile 0.1%
Replaces the app's console.* calls with a structured logger and ships server-side records to Marsad (marsad.space). Logging is server-side only. `src/lib/observability/log.ts` is the API everything uses — levels, fields, `child()` for binding scope, and pluggable sinks. `marsad.ts` is the only module that imports `@marsadco/sdk`, so replacing a pre-1.0, single-maintainer dependency stays a one-file change rather than a sweep through 90 call sites. `boot.ts` wires it up from env at startup. The console sink always runs and `scope` is promoted back to the familiar `[worker]` prefix, so `just dev` output reads as it did before; a sink that throws degrades to console-only rather than taking a request down. Migrated 20 call sites: worker loop, tasks and emails, root middleware, the auth/profile/upload routes, curation fan-in, POI storage, migrations, and three island error paths. `src/scripts/` keeps console.* deliberately — there the output is the product, not telemetry. Two decisions worth knowing. Network capture is off on the server: the SDK leaves bodies and headers off by default but still captures URLs, and in this app a URL is routinely a credential (Supabase `?token=` signed URLs, `?share=` tokens) while our outbound calls all carry auth. A `beforeSend` scrubber walks every event as a backstop. And the user is attached per-event rather than via the SDK's `setUser()`, which writes one field on one process-wide client — under concurrency that stamps request A's user onto request B's error. The root middleware builds a request-scoped `ctx.state.log` instead, reached via `logFor(ctx.state)`. There is deliberately no browser half. The SDK targets Next.js/Expo, where `init()` runs once per session; Fresh is an MPA, so every navigation would re-parse it, re-init, and send a fresh session ping and pageview. The island measured 12 KB gzip and its per-navigation execution and ingest chatter did not earn that on a personal travel log. Rationale and the path back are in `wiki/observability.md`. Pinned to 0.7.0: 0.8.0 is 2 days old and blocked by `minimumDependencyAge: P7D`. `just check` clean; `just test::units` 554 passed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .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 | ||
| build.just | ||
| CLAUDE.md | ||
| deno.json | ||
| deno.lock | ||
| Dockerfile | ||
| env.example | ||
| fjx.json | ||
| justfile | ||
| LEARN.md | ||
| migrate.just | ||
| README.md | ||
| supabase.just | ||
| test.just | ||
| trip.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::prod |
production build — Vite + the Lit component bundle |
just build::components |
rebuild src/static/components-map.js only (after editing src/lib/web-components/) |
just build::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-map.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 — and nothing else;just test::rulesfails on a custom element defined anywhere else, or rendered inside an island's JSX. 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 build::docker # docker build -t trippin .
There is no compose recipe: the docker-compose.yml the host runs is not committed — DEPLOY.md carries it and the Caddy config to paste onto the server.
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.