Skip to main content
Glama
LKRCharon

Field Current

by LKRCharon
README.md
# Field Current

Field Current is a small, self-hosted research library for keeping one narrow field current.
It keeps checked paper metadata, abstracts, review decisions, author/affiliation evidence,
and canonical BibTeX in one place, with a deliberately bounded read-only MCP surface for
research agents. It runs as either one Node process with one SQLite file or two optional
Cloudflare Workers backed by D1 and KV. The Node runtime supports an invite-only shared
laboratory library with individual accounts, roles, browser sessions, and personal MCP tokens.

This is the research layer beside a PDF reader or reference manager, not a replacement for
either one. It deliberately avoids
Postgres, Redis, object storage, vector search, browser extensions, and word-processor
plugins.

Existing installations retain the internal `bibhub` runtime identifiers for compatibility:
database paths, environment variables, Docker volumes, systemd paths, Cloudflare resources,
and MCP URLs do not change during the product rename.

## What it includes

- DBLP and arXiv title lookup with bounded responses, caching, and visible disagreements.
- BibTeX import/export, deterministic cite keys, `.bib`/TeX audit, and safe re-key plans.
- An arXiv discovery inbox. New works remain candidates until a curator or owner records a review.
- Bidirectional citation cross-checks from existing works, with every seed relation retained.
- Abstract display without downloading PDFs.
- Optional author and paper-time institution evidence from OpenAlex.
- A read-only MCP endpoint with exactly three tools: `lookup`, `entries`, and `bib`.

A fresh Node instance idles at roughly 115 MB RSS in the current test environment. Allow
256 MB RAM for a small installation. Disk use is the SQLite database plus a disposable
metadata cache; Field Current does not store PDFs.

## Docker quick start

Requirements: Docker Engine with Compose.

```sh
cp .env.example .env
chmod 600 .env
docker compose build
docker compose run --rm bibhub npm run users -- \
  bootstrap-owner --db /data/bibhub.db --username owner --display-name "Lab owner"
```

The bootstrap command prints a random temporary password once. Save it, then start the service:

```sh
docker compose up -d
docker compose ps
```

Open <http://127.0.0.1:8788>, sign in, and replace the temporary password. There is no web
self-registration and the first owner can only be created from the local CLI.

The container runs as a non-root user with a read-only root filesystem. Its database and
cache live in the named volume `bibhub-data`; the HTTP port binds to loopback by default.
Put Caddy, Tailscale, or a private tunnel in front of it if remote access is needed.

Stop and remove the process without deleting the library:

```sh
docker compose down
```

Do not add `-v` unless you intentionally want to delete the `bibhub-data` volume.

## Bare Node

Requirements: Node 22.18+ or Node 24.11+.

```sh
npm ci --omit=dev
umask 077
npm run users -- bootstrap-owner --db ./bibhub.db \
  --username owner --display-name "Lab owner"
BIBHUB_PUBLIC_ORIGIN=http://127.0.0.1:8788 npm run serve
```

The default listener is `127.0.0.1:8788`, the compatibility database name is
`./bibhub.db`, and the response cache is `./.cache`. Relevant environment variables are:

| Variable | Meaning |
|---|---|
| `AUTH_TOKEN` | Optional legacy/break-glass owner bearer token. `BIBHUB_TOKEN` remains a compatible Node alias. A new Node install uses accounts instead. |
| `BIBHUB_PUBLIC_ORIGIN` | Exact browser origin used for session and Origin checks; set the external HTTPS origin behind a proxy or tunnel. |
| `MCP_READ_TOKEN` | Optional shared MCP compatibility token. New Node installs use one `library:read` token per user. |
| `BIBHUB_HOST` / `PORT` | Listener, default `127.0.0.1:8788`. |
| `BIBHUB_DB` | SQLite path. |
| `BIBHUB_CACHE` | Disposable DBLP/arXiv cache directory. |
| `BIBHUB_WATCHLIST` | JSON array of arXiv discovery queries; default `[]` disables topic discovery. Cloudflare Workers accepts at most 5 terms per sweep to stay within the Free D1 query budget. |
| `BIBHUB_CITATION_DISCOVERY` | Scheduled citation scans only; default `false`. Manual scans remain available. |
| `BIBHUB_SWEEP_INTERVAL_HOURS` | Node/Docker interval, default `24`; `0` disables the timer. |
| `OPENALEX_API_KEY` | Optional free OpenAlex key for author enrichment and citation discovery. |
| `SOURCE_CODE_URL` | Exact HTTPS source URL for this running revision; required before exposing an AGPL network instance. |
| `SOURCE_REVISION` | Full 40- or 64-hex Git commit ID served by `SOURCE_CODE_URL`; required with it. |

[`deploy/bibhub.service.example`](deploy/bibhub.service.example) and
[`deploy/bibhub.env.example`](deploy/bibhub.env.example) provide a hardened systemd setup.

## Laboratory accounts

One Node deployment is one shared laboratory library. Accounts control who may act on it;
they do not create private copies of papers.

| Role | Access |
|---|---|
| `reader` | Browse, search, export BibTeX, and use read-only MCP. |
| `curator` | Reader access plus save/import and candidate review. |
| `owner` | Curator access plus discovery, OpenAlex refresh, and local user administration. |

Invite users from the host. Generated temporary passwords are printed once and must be
changed on first sign-in:

```sh
npm run users -- invite --db ./bibhub.db --actor owner \
  --username alice --display-name "Alice" --role curator
npm run users -- list --db ./bibhub.db
npm run users -- disable --db ./bibhub.db --actor owner --username alice
```

Passwords use salted Node `scrypt`; the database stores only password material, session
hashes, and API-token hashes. Browser sessions are `HttpOnly`, `SameSite=Strict`, and
`Secure` when `BIBHUB_PUBLIC_ORIGIN` is HTTPS. Unsafe cookie-authenticated requests must
match that configured Origin. Disabling a user or changing their password invalidates their
sessions and API tokens.
There is no public registration or “first visitor becomes admin” route.

The canonical paper rows remain global. A future multiple-direction feature should add
`tracks` and membership/link tables around those works; the compatibility `repos` field is a
collection label, not a tenant or permission boundary.

## L40-style shared-server deployment

On a shared compute server, keep the release and SQLite/WAL on a local data filesystem rather
than a nearly-full system disk or network filesystem. Run the process as a dedicated Unix user,
bind only to `127.0.0.1`, and put an HTTPS reverse proxy or outbound tunnel in front. A typical
layout is:

```text
/data/field-current/releases/<source-revision>
/data/field-current/state/bibhub.db
/data/field-current/state/cache/
```

Set `BIBHUB_PUBLIC_ORIGIN` to the exact external HTTPS origin. Do not expose the Node port on
`0.0.0.0`; MCP is served by the same process at `/mcp`. Back up the SQLite database with the
built-in online backup command and verify the snapshot before every upgrade.

## Discovery topics

No topic is enabled by default, so a new installation does not make scheduled arXiv
requests. Node and Docker accept up to 20 queries; Cloudflare Workers accepts up to 5 so
one sweep stays inside the Free D1 query budget. Each query may be at most 120 characters:

```sh
BIBHUB_WATCHLIST='["database provenance","scientific metadata"]'
```

Node and Docker run configured discovery once per interval when either the watchlist or
scheduled citation discovery is enabled; the default is 24 hours after process start.
`npm run sweep` performs one configured discovery pass and exits. The Cloudflare build runs
its configured discovery daily. A match enters the review inbox; it never enters the formal
library automatically.

## Citation discovery

Citation discovery uses checked library entries as seeds and looks in both directions:
works referenced by a seed and works that cite it. Results enter the same review inbox;
they are never promoted into the formal library without an explicit review. When several
seeds find the same candidate, the inbox keeps every seed and relation instead of creating
duplicate candidates.

The browser's **Scan citations** action runs one authenticated seed at a time. Keeping the
deployed Worker batch at one leaves headroom under Workers Free CPU and D1 query limits; the
self-hosted Node endpoint may opt into a batch of up to three with its `limit` parameter.
The same action is available over HTTP:

```sh
curl -X POST 'http://127.0.0.1:8788/api/citations/scan?limit=1' \
  -H "Authorization: Bearer $AUTH_TOKEN"
```

That bearer example is for Cloudflare or a deliberately configured legacy owner token. On a
multi-user Node instance, owners use the browser session; personal MCP tokens are read-only
and are rejected on this write route.

Manual scans are available whenever a free `OPENALEX_API_KEY` is configured, regardless of
`BIBHUB_CITATION_DISCOVERY`. Set that flag to `true` only if scheduled scans are wanted.
On Cloudflare, keep citation discovery on its dedicated `BIBHUB_CITATION_CRON` trigger rather
than combining it with the arXiv sweep; the supplied Wrangler config uses 06:20 and 06:50 UTC.

OpenAlex citation data is useful for finding omissions, not for authoritative citation
counts: coverage can be incomplete, delayed, or missing for an otherwise valid work. Seeds
without a DOI, arXiv identity, or verified OpenAlex authorship snapshot are checkpointed as
unsupported; their formal entries are not changed. Field Current treats the configured key as
free-only: before every actual OpenAlex list or search attempt, including retries, it checks
the endpoint-specific cost against the free daily allowance and keeps a US$0.10 reserve.
Citation scans also use a database lease so manual and scheduled batches cannot race. Prepaid
balance is never read or used.

## Import and command-line tools

The browser can look up and save an individual work or paste a complete `.bib` into a
collection. The same batch import is available over the authenticated API:

```sh
curl -X POST http://127.0.0.1:8788/api/import \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary "$(jq -n --rawfile bib refs.bib --arg repo reading-list '{bib:$bib,repo:$repo}')"
```

The curl example uses the optional legacy owner token. The normal multi-user Node path is an
owner/curator browser session, so a leaked MCP token can never import or review papers.

Local, read-mostly bibliography tools need no running server:

```sh
npm run cli -- selftest
npm run cli -- key --author "Vaswani, Ashish" --year 2017 --title "Attention Is All You Need"
npm run cli -- lookup "Attention Is All You Need"
npm run cli -- audit --bib refs.bib --tex paper.tex
npm run cli -- rekey --bib refs.bib --tex paper.tex
```

`audit` exits non-zero on missing, uncited, duplicate, or off-rule keys. `rekey` only prints
a plan unless `--apply` is supplied, then rewrites the BibTeX and every supplied TeX file
from one checked plan. The cite-key policy is documented in
[`docs/citation-rules.md`](docs/citation-rules.md).

The database column and API parameter are still named `repos`/`repo` for compatibility;
the browser presents these values as collections.

## Backups

Create a consistent SQLite online backup while the Node service is running:

```sh
npm run backup
npm run verify:backup -- /path/to/bibhub-2026-08-06T06-20-00Z.sqlite
```

By default the snapshot is written under a `backups/` directory beside the live database,
with mode `0600`. In Docker, run `docker compose exec bibhub npm run backup`; the snapshot
stays in the persistent `/data` volume. Copy verified snapshots to a second machine or
encrypted storage (`docker compose cp bibhub:/data/backups/<file>.sqlite ./` copies one out).
Restore only while Field Current is stopped: move the old database and its `-wal`/`-shm` sidecars
aside together, install a verified snapshot at the configured database path, then start the
service and check `/readyz`.

## Optional author and affiliation evidence

Author enrichment is an explicit authenticated write, never a side effect of an MCP read.
If `OPENALEX_API_KEY` is unset, the library continues to work and enrichment reports that it
is disabled. Configure only a free OpenAlex key; Field Current contains no paid provider or
billing fallback. Every billable list or search attempt stops before the protected US$0.10
free reserve.

```sh
curl -X POST http://127.0.0.1:8788/api/authorships?limit=5 \
  -H "Authorization: Bearer $AUTH_TOKEN"
```

Affiliations are institutions reported on a particular paper record, not a claim about an
author's current employer. Stored evidence includes source work and author identifiers,
retrieval time, match method, ROR/OpenAlex institution identifiers, and raw affiliation
strings. Missing evidence means “not checked”; an empty checked result means the source
supplied no affiliation.

Agents request the same analysis through `entries({view:"authors", author:"..."})`. Results
keep paper abstracts, coauthors, venues, collections, coverage, and work-scoped institutions
together so an agent does not have to infer employment from a name.

## Cloudflare deployment

Cloudflare is optional. The public Wrangler template contains no account or resource IDs.
Keep the instance copy ignored so automatic provisioning cannot add your IDs to a public
commit:

```sh
npm ci
if [ ! -e wrangler.production.jsonc ] && [ ! -e wrangler.mcp.production.jsonc ]; then
  cp wrangler.jsonc wrangler.production.jsonc
  cp wrangler.mcp.jsonc wrangler.mcp.production.jsonc
  chmod 600 wrangler.production.jsonc wrangler.mcp.production.jsonc
else
  echo "keeping existing instance configs"
fi
npm run types
npm run check
npm test
npm run deploy
```

The copy step is for a brand-new instance only. During an upgrade, keep both ignored
production files: they contain the bindings that point at the existing D1, KV, service, and
rate-limit resources. Replacing either file can bind the next deploy to an empty resource.

The first deploy creates the D1 database and KV namespace and writes their IDs only into
the ignored `wrangler.production.jsonc`. API access remains fail-closed until a secret is
set. Initialize the database, then add the token:

```sh
npm run db:remote
npx wrangler secret put AUTH_TOKEN -c wrangler.production.jsonc
npx wrangler secret put OPENALEX_API_KEY -c wrangler.production.jsonc  # optional, free key only
```

Before exposing the AGPL version, add the same immutable `SOURCE_CODE_URL` and
`SOURCE_REVISION` string variables to both private Wrangler configs. Existing main-Worker
configs must also add `/source` to `assets.run_worker_first`. The main page's `/source`
route and the MCP initialization metadata must identify the same running source.

Both deployment commands run a fail-closed source preflight before Wrangler. It checks both
ignored production configs, even when only one Worker is being deployed. Run the same check
without deploying with `npm run source:preflight`. In a Git checkout, the local Field Current
source scope must be clean; unrelated files elsewhere in a containing monorepo are outside the
check. A standalone source export without `.git` still receives all config checks.

The preflight cannot prove that an external `SOURCE_CODE_URL` contains the running files, and
does not compare its revision with a private monorepo's unrelated `HEAD`. The publisher remains
responsible for exporting the exact running source, committing it to the public repository, and
putting that public commit ID in both production configs.

Set `BIBHUB_WATCHLIST` in the private config if scheduled topic discovery is wanted. Set
`BIBHUB_CITATION_DISCOVERY` to `true` only if scheduled citation scans are wanted, then
redeploy. The citation flag does not disable authenticated manual scans.
Cloudflare's free-plan limits can change; verify the current plan before relying on it. The
application does not bind R2 or require a payment method.

For a database created before the public migration ledger, inspect and apply only the
needed one-time files described in
[`manual-upgrades/README.md`](manual-upgrades/README.md). Fresh databases need only
`npm run db:remote`; the Node server applies the same ordered migration files at startup and
refuses to listen if one fails or an already-applied file has changed.

## Read-only MCP Worker

The MCP Worker follows protocol version `2026-07-28` using the TypeScript SDK v2 and
Streamable HTTP at `/mcp`. Its public capability surface is fixed:

| Tool | Access |
|---|---|
| `lookup` | Read-only DBLP/arXiv verification. |
| `entries` | Read-only paper search and bounded author/affiliation analysis. |
| `bib` | Read-only canonical BibTeX rendering. |

It has no D1, KV, assets, cron, or general fetch binding. A named service binding can call
only the main Worker's `ReadEntrypoint` methods, and `MCP_READ_TOKEN` is separate from the
write-capable `AUTH_TOKEN`.

Deploy the main Worker first. Before continuing, store the generated MCP token in a password
manager or configure the client that will use it; the shell variable is deliberately removed
after deployment and cannot be recovered from Cloudflare:

```sh
npm run types:mcp
npm run check
mcp_read_token="$(openssl rand -base64 48)"
printf 'Save this MCP token now:\n%s\n' "$mcp_read_token"
printf 'MCP_READ_TOKEN=%s\n' "$mcp_read_token" \
  | npm run deploy:mcp -- --secrets-file /dev/stdin
unset mcp_read_token
```

For a new account, choose an unused integer `namespace_id` in the private MCP config before
deploying. Existing instances must retain their current private namespace instead of copying
the public example again.

Clients call `https://bibhub-mcp.<your-workers-subdomain>.workers.dev/mcp` with
`Authorization: Bearer <MCP_READ_TOKEN>`.

## Self-hosted Node MCP

The Node server exposes the same stateless MCP `2026-07-28` endpoint at `/mcp`, backed by a
separate read-only SQLite connection. A signed-in user creates a named MCP token in the account
panel, copies it once, and configures the client with:

```text
Authorization: Bearer fcpat_...
```

Each token is independently revocable, limited to `library:read`, and rate-limited separately.
The three-tool boundary remains `lookup`, `entries`, and `bib`; the token is rejected on HTTP
write routes even when its owner is a curator or owner.

This personal access-token lane is intended for preconfigured laboratory clients. It is not an
interactive OAuth authorization server. For arbitrary third-party MCP clients that need browser
sign-in, deploy a standards-compliant OAuth 2.1 authorization server and Protected Resource
Metadata in front of the endpoint; do not invent a token exchange or pass another service's
access token through Field Current.

## HTTP routes

Node `/api/*` routes accept an authenticated user session and enforce the role table above;
read-only personal tokens may call GET routes only. Cloudflare and compatibility-mode Node
deployments use their configured bearer token. Static assets and `/healthz` do not. The
Node/Docker `/readyz` endpoint is unauthenticated for local health checks; Cloudflare
`/readyz` requires `AUTH_TOKEN` so an unauthenticated caller cannot spend D1 reads. Health
responses contain no database path, counts, or instance settings. `/source` is public by
design and redirects only to the configured exact Corresponding Source revision.

| Method and path | Purpose |
|---|---|
| `GET /source` | Redirect to the exact Corresponding Source for the running AGPL version. |
| `POST /api/auth/login` / `POST /api/auth/logout` | Create or revoke a Node browser session. |
| `GET /api/me` / `POST /api/auth/change-password` | Read the current account or replace a temporary password. |
| `GET/POST/DELETE /api/tokens...` | Manage the current user's read-only MCP tokens. |
| `GET /api/lookup?title=...` | Compare DBLP and arXiv metadata. |
| `GET /api/entries?q=...&repo=...` | Browse checked entries. |
| `POST /api/entries` | Add or update one checked entry. |
| `POST /api/import` | Import a BibTeX batch into a collection. |
| `GET /api/bib?repo=...` | Export BibTeX. |
| `POST /api/audit` / `POST /api/rekey` | Audit or plan/apply cite-key changes. |
| `GET /api/candidates?state=new&limit=50&offset=0` | Read a page of the discovery inbox. |
| `POST /api/candidates/review` | Record import/ignore and atomically promote imports. |
| `POST /api/citations/scan?limit=1` | Cross-check citations from checked library seeds. |
| `POST /api/sweep` | Run discovery on demand. |
| `POST /api/authorships?limit=...` | Refresh optional OpenAlex evidence. |

## Public-source boundary

The source distribution contains no library records, production resource IDs, review
history, token, API key, database, cache, or author evidence. `npm run public:check` rejects
common instance files, project-specific names, literal credentials, and Cloudflare resource
IDs before release.

Do not publish this directory with inherited repository history: old commits can retain data
that no longer exists in the current tree. Export the checked tree into a new repository with
a new first commit and a public/noreply Git identity. Do not zip the working directory; ignored
files are present locally. Export a safe, history-free tree with:

```sh
npm run export:public -- ../field-current-public
```

The command refuses to overwrite an existing destination, copies an explicit allowlist,
and runs the public check again inside the exported tree. Initialize a new repository there
and make a new first commit with a public/noreply Git identity.

## Development

```sh
npm ci
npm run build:css
npm run types
npm run types:mcp
npm run check
npm test
npm run public:check
npm run export:public -- /path/to/new-empty-directory
```

The Node and Worker paths share bibliography, source, rendering, and validation modules.
Keep MCP limited to `lookup`, `entries`, and `bib`; write operations belong only to the main
authenticated service.

## License and corresponding source

Field Current is licensed under `AGPL-3.0-only`; see [`LICENSE`](LICENSE),
[`NOTICE`](NOTICE), and [`THIRD_PARTY_NOTICES`](THIRD_PARTY_NOTICES). This directory is an
independently licensed subproject when it appears inside a larger repository.

If you run a modified version and let users interact with it over a network, AGPL section 13
requires that those users receive a prominent, no-charge opportunity to obtain that running
version's Corresponding Source. Field Current exposes `/source` in the web UI and publishes
the same source location in MCP server metadata. Before exposing an instance, set both:

```sh
SOURCE_CODE_URL='https://code.example/field-current/tree/<full-commit-id>'
SOURCE_REVISION='<full-commit-id>'
```

The URL must identify the source for the version actually running, including a fork's
modifications. A tag, moving branch, signed URL, or abbreviated hash is insufficient: the
revision must be a full Git SHA-1 or SHA-256 object ID and an exact path segment of the
query-free HTTPS URL. If either value is absent or invalid, `/source` fails closed with
`503`. Bibliographic records, notes, PDFs, credentials, and other instance data are not
part of the software source distribution.