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/exampleSQLite 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
- 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.
- 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.jsoncannot touch them (OPERATOR_ONLY_FEATURES, below). Of the rest:mail,whatsappandwhatsappInboundare channels a journal can mute below the server's own default.extract,routeRecordingandmapReliefwork the other way — the server allows them, and a journal must opt in before they do anything for that journal. - Reading needs nothing. With
authoff, 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. - No paid account to develop. Mail can write
.emlfiles, and most providers have adry-runbackend.helper,extractandtranscriptionare 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.
| Feature | Needs | Off means |
|---|---|---|
auth | SESSION_SECRET and a database | no agent tokens or sessions, so no writing and no readers at all |
signup | SESSION_SECRET, a database, and mail for whichever phoneBackend it is configured with | nobody can create a journal on the instance |
contacts | CONTACTS_ENCRYPTION_KEY, a database and auth | no 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 |
addressLookup | nothing for the default backend (photon); ADDRESS_LOOKUP_API_KEY for any other backend | no address suggestions in a contact form |
analytics | a database | no visits page |
logging | — (operator only) | no request logging |
Telling readers
| Feature | Needs | Off means |
|---|---|---|
mail | file and console transports need nothing; smtp needs SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, MAIL_FROM | nothing is sent |
push | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT | no web-push notifications |
applePush | nothing for the dry-run backend (the payload is written under <dataDir>/apns/); APNS_KEY_ID, APNS_TEAM_ID, APNS_KEY for the real apns backend | no notification reaches an iPhone |
sms | dry-run needs nothing; twilio needs TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER | no text messages |
smsInbound | TWILIO_AUTH_TOKEN and a database | incoming texts are not read |
The assistant
| Feature | Needs | Off means |
|---|---|---|
helper | ANTHROPIC_API_KEY and a database | no writing assistant |
transcription | a database; dry-run needs nothing else, deepgram needs DEEPGRAM_API_KEY | no dictation |
extract | SESSION_SECRET, auth and helper, and a journal must switch it on for itself | no guided import of photos into draft days from the studio |
credits | a 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
| Feature | Needs | Off means |
|---|---|---|
routeRecording | nothing of its own, and a journal must switch it on for itself | the 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".
| Feature | What it is at fernscout.ch |
|---|---|
photobook | a trip laid out and printed as a book |
postcards | real printed cards to readers' addresses |
whatsapp | new-day messages and the guided assistant on WhatsApp |
whatsappInbound | the 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 needsphotobookto be enabled (REQUIREMENTSinlib/capabilities.ts), so with nophotobookin 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.


