Hosting

One server with Node behind Caddy, and one deploy script. Reading a public journal needs no database; writing and guests do. Every optional capability is off until you configure it.

Run it locally

npm install
npm run dev            # http://localhost:3000/example

SQLite locally, Postgres in production. Nothing needs a paid account to develop or test: mail can write .eml files, and every provider has a dry-run backend.

Optional capabilities

  1. Switching one on is a promise. A feature that is on but missing its credentials refuses to boot and says why, rather than half-working.
  2. A journal narrows the server's choice, never widens it — and only for the few features it has any say over at all. Most features are decided once, by the operator, for the whole instance; a journal's own config.json cannot touch them (OPERATOR_ONLY_FEATURES, below). Of the rest: mail, whatsapp and whatsappInbound are channels a journal can mute below the server's own default. extract, routeRecording and mapRelief work the other way — the server allows them, and a journal must opt in before they do anything for that journal.
  3. Reading needs nothing. With auth off, every public page, the search, the feed and the sitemap still work, because a public trip never touches a database or a session. You lose writing and readers.
  4. No paid account to develop. Mail can write .eml files, and most providers have a dry-run backend. helper, extract and transcription are the exceptions below.

What each one needs

These are the same features the table above classifies; this section says what makes each one boot and what its absence looks like.

FeatureNeedsOff means
authSESSION_SECRET and a databaseno agent tokens or sessions, so no writing and no readers at all
signupSESSION_SECRET, a database, and mail for whichever phoneBackend it is configured withnobody can create a journal on the instance
contactsCONTACTS_ENCRYPTION_KEY, a database and authno readers, invite links or approval queue
reactions—no reactions on days
costs—no cost pages or totals
weather—a day's weather: true is never looked up
addressLookupnothing for the default backend (photon); ADDRESS_LOOKUP_API_KEY for any other backendno address suggestions in a contact form
analyticsa databaseno visits page
logging— (operator only)no request logging

Telling readers

FeatureNeedsOff means
mailfile and console transports need nothing; smtp needs SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, MAIL_FROMnothing is sent
pushVAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECTno web-push notifications
applePushnothing for the dry-run backend (the payload is written under <dataDir>/apns/); APNS_KEY_ID, APNS_TEAM_ID, APNS_KEY for the real apns backendno notification reaches an iPhone
smsdry-run needs nothing; twilio needs TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBERno text messages
smsInboundTWILIO_AUTH_TOKEN and a databaseincoming texts are not read

The assistant

FeatureNeedsOff means
helperANTHROPIC_API_KEY and a databaseno writing assistant
transcriptiona database; dry-run needs nothing else, deepgram needs DEEPGRAM_API_KEYno dictation
extractSESSION_SECRET, auth and helper, and a journal must switch it on for itselfno guided import of photos into draft days from the studio
creditsa database (operator only)model calls and sends are never metered

extract is the studio's guided camera-roll photo import; it does not read bank or card statements. Statement reading (/studio/statement and POST /api/v2/{user}/statements) needs no feature flag in v2 at all — it is reachable whenever a journal's owner is signed in.

With credits on, the assistant's calls are metered against a journal's balance. With it off, the assistant is the operator's own and unmetered. Buying credits is hosted edition only, through @paid/credits; the ledger (lib/credits.ts) and spending from it are open.

Location

FeatureNeedsOff means
routeRecordingnothing of its own, and a journal must switch it on for itselfthe iPhone app records no GPS history, and the two owner-cookie doors that read it back (a place name for a day, the owner's own recorded route) stay closed

Hosted edition only

Four features — photobook, postcards, whatsapp, whatsappInbound — are named in FEATURE_NAMES so that one configuration file works for both editions, but their code lives in the private repository (PAID_FEATURES in lib/capabilities.ts). Switching one on in the open edition refuses to boot with "features.… is enabled but it is not included in this build".

FeatureWhat it is at fernscout.ch
photobooka trip laid out and printed as a book
postcardsreal printed cards to readers' addresses
whatsappnew-day messages and the guided assistant on WhatsApp
whatsappInboundthe guided assistant reading WhatsApp messages

One more feature builds on those but is not itself refused in the open edition, because it behaves differently when the feature it depends on is absent:

  • mapRelief — the shaded relief layer on a photobook's route map. It needs photobook to be enabled (REQUIREMENTS in lib/capabilities.ts), so with no photobook in this build it is simply never reachable; switching it on by itself boots fine and does nothing.

A running instance explains its own state at /api/health.

What a day looks like

One JSON file per day, in content/<username>/trips/<trip-id>/entries/YYYY-MM-DD-slug.json:

{
  "title": "Lanterns of Hoi An",
  "date": "2026-08-26",
  "time": "16:45",
  "timezone": "Asia/Ho_Chi_Minh",
  "location": "Hoi An",
  "country": "Vietnam",
  "countryCode": "VN",
  "coordinates": { "lat": 15.8801, "lng": 108.338 },
  "content": "The diary text, in plain markdown.\n\nBlank lines and all.",
  "transportMode": "bus",
  "transportFrom": "Da Lat",
  "transportTo": "Hoi An",
  "media": [
    { "src": "/media/<trip-id>/hoi-an/01.jpg", "type": "image", "width": 1200, "height": 800 }
  ],
  "costs": [
    { "label": "Dinner", "amount": 180000, "category": "food", "currency": "VND" }
  ],
  "status": "draft"
}

Only "status": "published" puts a day on the site; anything else, including no status at all, reads as a draft. A file that isn't valid JSON is skipped and logged, and the rest of the trip still shows. A trip's trip.json holds its title, dates, travellers, budget, planned route, exchange rates and visibility: private, public or guest. An unknown visibility reads as private, so a typo can't publish somebody's trip.

Deploying

A server with backups is a longer walk than this page, and it lives in the repository beside the code it deploys.

Every capability, with what it needs, is in docs/capabilities.md.