Logs¶
notefeed writes its logs to standard output as JSON lines: one object per line, with the level as a word (level), an ISO time (time), the part of notefeed that logged it (component), a fixed message (msg) and fields with the details. For example, the line at start-up:
{"level":"info","time":"2026-10-02T09:00:00.120Z","component":"startup","node":"v22.23.3","dataDir":"/data","passwordSet":true,"oidcProviders":1,"publicUrlSet":true,"trustProxy":true,"maxFeeds":0,"maxNotesPerFeed":0,"maxImagesPerFeed":0,"logLevel":"info","logRequests":true,"metrics":false,"msg":"notefeed started"}
NOTEFEED_LOG_LEVEL sets how much is logged: error, warn, info (the default), debug or silent. Each level includes the ones before it. A value that isn't a level counts as info, and the start-up log warns about it.
Read the logs with docker compose logs notefeed, or your container runtime's equivalent; docker compose logs -f notefeed follows them. For lines a person can read, pipe them through pino-pretty, which npx fetches on first use: docker compose logs --no-log-prefix notefeed | npx pino-pretty. To pick lines out, filter on the JSON, for example docker compose logs --no-log-prefix notefeed | grep '"level":"warn"', or with jq. Next.js prints a few plain lines of its own when the server starts, and a plain line for an error of its own; its error for a reader who left while a page was being sent (The destination stream closed early.) is left out, because the request line already says aborted.
The request line¶
Every request notefeed answers gets one line, written by notefeed itself when the response is done:
{"level":"info","time":"2026-10-09T10:00:00.120Z","component":"http","req":"q0Zr1fJx","method":"POST","route":"/api/v1/feeds/[feed]/notes","status":201,"ms":12,"bytes":412,"msg":"request"}
| Field | Example | What it is |
|---|---|---|
req |
q0Zr1fJx |
A random id for this request. Every other line logged while the request was answered has the same req, so a note posted or a request failed can be matched to its request. |
method |
GET |
The request's method. |
route |
/[feed], /api/v1/feeds/[feed]/notes, /r/[readId]/feed.xml, /mcp |
The pattern of the route that answered, never the path that was asked for. A post to /<feed> is logged as the API operation it is, /api/v1/feeds/[feed]/notes. Absent for a path that is none of notefeed's. |
status |
200 |
The status of the answer. |
ms |
12 |
The time from the request's arrival to the end of the answer, in milliseconds. |
bytes |
4312 |
The bytes sent for the answer, its headers included. |
outcome |
rate_limited, auth, not_found |
Only for a refusal: why. It is the code of the API's error body (auth, rate_limited, too_many_attempts, not_found, invalid_feed, too_large and so on); auth too when a locked instance sends a visitor to the login page; method_not_allowed; error for an unexpected failure (a 500, which has its own request failed line); aborted when the client went away before the answer was complete. |
The line never holds the path, the query, the feed name, a read id, the client's address, the user agent, the referrer, a header or anything of the body.
The app's own scripts, styles and icons ("route":"/_next/*" and "route":"/[file]") the scrape of /metrics and the probe of /health are logged at debug, so that info shows what people and scripts do. NOTEFEED_LOG_REQUESTS=0 turns the request line off and leaves every other line as it is; NOTEFEED_LOG_LEVEL=warn hides it together with the other info lines.
Your reverse proxy's access log¶
With the request line you do not need the proxy's access log to see what the instance does, and the proxy's log is the risky one: the proxy sees the whole path, and the path holds the feed name, which works like a password. Stored next to the client's address, it says who reads and posts to which feed. Turn the proxy's access log off, or make it leave out the path, the query and the client's address. Replacing feed names by patterns in the proxy is fragile: the patterns have to follow every route notefeed adds.
The other lines¶
| Level | component |
msg |
When |
|---|---|---|---|
info |
http |
request |
Every request: see The request line. debug for static files, /metrics and /health. |
info |
feeds |
feed created, feed deleted |
A feed came to exist with its first note, or was deleted with everything in it; protected says whether it was created with a feed password. Never its name. |
info |
notes |
note posted, note deleted |
A note was stored or deleted. kind is markdown or image, and a posted note has its size in bytes; a picture sent with a text is a note posted of its own. Never the feed, the note's id or its title. |
info |
storage |
image stored in the image store, image deleted from the image store |
With image bytes kept outside the database: an image's bytes were written to the image store (bytes is their size) or removed from it, when its note was posted, replaced or deleted or its feed was deleted. Never the object's key. |
info |
auth |
password login succeeded |
The instance password was right on the login page. |
info |
startup |
notefeed started |
Once, at start-up, with the Node version, DATA_DIR, whether the instance password, PUBLIC_URL and NOTEFEED_TRUST_PROXY are set, the number of sign-in providers, the caps, the log level and whether requests are logged. |
warn |
startup |
sign-in provider configured partially; it stays off |
At start-up, for each sign-in provider with some but not all of its four variables set; missing names the variables that are missing. |
warn |
startup |
sign-in is on without PUBLIC_URL: the redirect URI comes from each request's Host header |
At start-up, when sign-in is on and PUBLIC_URL is not set. |
warn |
startup |
NOTEFEED_LOG_LEVEL is not a level; using info |
At start-up. |
warn |
feeds |
reserved feeds are listed but have no password; they are not created |
At start-up, when NOTEFEED_RESERVED_FEEDS is set and NOTEFEED_RESERVED_PASSWORD is not. |
warn |
feeds |
reserved feed exists but is not protected, or reserved feed exists with another read id than its name |
At start-up, for a reserved feed that already existed as an ordinary feed when its name was listed; feed is the reserved name and hint says what to do: delete its folder in DATA_DIR and restart, so it is recreated protected with its name as read id. With NOTEFEED_RESERVED_PASSWORD set, this check makes notefeed read its feeds and create the missing reserved feeds at start-up, rather than at the first request. |
info |
oidc |
sign-in succeeded |
A sign-in worked; provider is the provider's id. |
warn |
oidc |
why a sign-in failed | See What the log says. |
info |
oauth |
authorization granted |
An MCP client got access; via is password or the sign-in provider's id. |
warn |
oauth |
refresh refused |
An MCP client's sign-in ended, so its refresh token no longer works; reason is sign-in switched off or sign-in past its lifetime. |
warn |
auth |
password login failed |
A wrong instance password on the login page ("page":"login") or on an MCP client's authorize page ("page":"authorize"). Refusals for too many attempts are not logged at this level. |
warn |
limits |
cap reached |
A post was refused by NOTEFEED_MAX_FEEDS, NOTEFEED_MAX_NOTES_PER_FEED or NOTEFEED_MAX_IMAGES_PER_FEED; kind is feed, note or image. |
warn, error |
feeds |
a feed's read id or a leftover folder | A problem with a feed's files that notefeed works around, such as a .readid it can't read; the feed's name is never in it. |
error |
http, mcp |
request failed, tool failed |
An unexpected failure, answered with a bare 500 or internal error; err carries the error, with everything below the data directory in a path replaced by <path> (so no feed name or note title). |
error |
http |
an operation answered a status it doesn't declare |
A bug: an API operation answered a status its OpenAPI description doesn't list, and the client got a bare 500; operation is the operation's id and status the status it tried to send. |
error |
health |
storage check failed, image store check failed |
A probe of /health found the storage or the image store not answering and got 503; err carries the error. |
error |
posting |
a picture of a failed post could not be removed |
A post or an edit of a note with pictures failed while storing, and one of the picture notes it had already stored could not be removed again: that picture may be left in the feed (the removal also reports a file that was already gone) and can be deleted by hand. note is the picture note's id (its file in DATA_DIR starts with it), and err carries the error when the removal itself failed; the feed's name is never in it. |
debug |
limits |
rate limit reached |
A request over NOTEFEED_RATE_LIMIT; kind is post or password. |
debug |
auth |
password bearer refused |
A script sent a wrong instance password as its bearer token. |
What is never logged: note content, feed names (they work like passwords; a reserved feed's name, which you chose, is the one exception), passwords, tokens, secrets, authorization codes, state and nonce values, cookies, e-mail addresses, names and other claim values, the name an MCP client gave itself, client IP addresses, and request headers or bodies. The request line holds a route's pattern, never its path.