@baseform/viewer-selfhosted
v0.1.1
Published
Optional self-hosted response viewer for BaseForm
Readme
@baseform/viewer-selfhosted
Optional self-hosted viewer for BaseForm responses. Ships as one Node process that serves both a Svelte SPA and a small Fastify API for ingest, list, detail, and export — no database, no container.
The viewer is a companion to BaseForm, not a requirement. You can build and ship forms without ever running it.
Modes
The viewer can receive responses three ways, all of which produce the
same NormalizedResponse records on disk:
| Mode | How you get data in |
| ------- | --------------------------------------------------------------------------------------------------- |
| Webhook | Point the Builder's "Standard webhook" transport at POST /ingest on the viewer |
| Bundle | Drop JSON files produced by the Builder's "Local download" transport into the configured bundle dir |
| Feed | Import a .json array or .jsonl file emitted by a custom pipeline |
Storage
Responses are appended to an on-disk JSONL file under
$VIEWER_DATA_DIR/responses.jsonl. Dedup is by content hash, so
replays from the same transport produce a single row. Corrupt lines
encountered on startup are quarantined to <path>.bad instead of
aborting. Form schemas live under $VIEWER_DATA_DIR/schemas/ and
power field-label rendering on the detail view.
Environment
| Variable | Default | Purpose |
| ---------------------- | --------------- | --------------------------------------------------------------------- |
| VIEWER_HOST | 127.0.0.1 | Bind host |
| VIEWER_PORT | 5175 | Bind port |
| VIEWER_DATA_DIR | ./viewer-data | Where responses and schemas are persisted |
| VIEWER_INGEST_SECRET | (unset) | If set, /ingest requires header x-baseform-ingest-secret: <value> |
| VIEWER_BUNDLE_DIR | (unset) | Optional bundle directory to scan at startup |
| VIEWER_FEED_FILE | (unset) | Optional .json or .jsonl feed to import at startup |
| VIEWER_LOG_LEVEL | info | Pino log level |
| VIEWER_SERVE_STATIC | true | Serve the built SPA from the same port as the API |
The ingest secret is read only from the environment; it is never
written to disk and never logged. Ingest route logs contain only
shape metadata ({ status }) — never request bodies or response
values. When the secret is enabled, /ingest still uses the
x-baseform-ingest-secret header, while every other viewer route
(SPA, list/detail/forms/export, bundle/feed APIs) is protected with
HTTP Basic auth using that same secret as the password.
Running
# Dev: Vite + Fastify side-by-side (hot reload client, proxies /ingest + /api/*)
pnpm --filter @baseform/viewer-selfhosted dev
# Prod: build SPA + server, then run
pnpm --filter @baseform/viewer-selfhosted build
pnpm --filter @baseform/viewer-selfhosted startAPI
POST /ingest— canonicalSubmissionPayloadJSON body; returns202 { id, duplicate }GET /api/responses— list, supportsformId,text,since,until,limit,offsetGET /api/responses/:id— detailGET /api/forms— imported canonical form schemas with normalized{ formId, schemaVersion }POST /api/forms— import a canonical BaseForm schema JSONPOST /api/ingest/bundle— scan a bundle directory on the server filesystemPOST /api/ingest/feed— import a.jsonor.jsonlfeed on the server filesystemGET /api/export?format=json|csv— export filtered responses
Tests
pnpm --filter @baseform/viewer-selfhosted test— Fastify integration suite viafastify.injectpnpm --filter @baseform/viewer-selfhosted e2e— Playwright golden-path: build, start Fastify, ingest → list → detail → export
