Stop dumping CI and agent test results into public HTML reports. Evidence Browser renders zip evidence bundles in an auth-gated, VS Code-like browser for teams reviewing logs, screenshots, reports, and run artifacts.
- Bundle viewer — Browse zip bundles with file tree navigation
- Rich rendering — Markdown (with embedded images), syntax-highlighted code, image preview
- Pluggable storage — Local filesystem or S3/R2-compatible object storage
- Authentication — Built-in username/password auth, API keys, and optional OIDC SSO
- AI Agent integration —
/llm.txtendpoint and MCP server for programmatic access - Hierarchical bundle IDs —
org/repo/pr-42/run-1maps to nested storage paths - First-run setup —
/setupguides the initial admin, storage check, and workspace creation ebCLI — Log in with an API key, create/validate bundles, upload evidence, and manage workspaces
Run the published image with a random session secret and local bundle storage:
docker run -p 3000:3000 \
-e AUTH_SECRET="$(openssl rand -base64 32)" \
-v "$PWD/data:/data" \
ghcr.io/hojinzs/evidence-browser:latestOpen http://localhost:3000 for the web app.
AUTH_SECRET signs admin sessions and must be a real random value for any
shared or production instance.
On a new instance, the app redirects to /setup until the first admin account
and workspace exist.
# Install dependencies
npm install
# Copy and configure environment
cp .env.example .env.local
# Start API + web development servers
npm run devOpen http://localhost:3000 for the web app.
In a workspace, click Load demo bundle or run eb upload examples/sample.zip --workspace default to render the bundled sample immediately.
- Web dev server:
http://localhost:3000(Vite) - API dev server:
http://localhost:3001(proxied as/apifrom the web app)
The /setup wizard appears automatically when an instance has no real admin
or no workspace. It walks through:
- Admin — create the first username/password admin account.
- Storage — verify the configured local or S3/R2 storage adapter can be read.
- Workspace — create the first workspace slug, name, and optional description.
After setup, sign in with the admin account, open Settings to create an API
key, then use that key with the eb CLI.
Install the CLI:
npm install -g evidence-browser-cliSave your server URL and API key locally:
eb login http://localhost:3000
# API key: eb_...Create and upload a bundle:
eb bundle create examples/sample-bundle --output /tmp/sample.zip
eb bundle validate /tmp/sample.zip
eb upload /tmp/sample.zip --workspace default --bundle-id sampleFor CI, skip the local config file and pass credentials through environment variables:
EB_URL=https://evidence.example.com \
EB_API_KEY=$EVIDENCE_BROWSER_API_KEY \
eb upload dist/evidence.zip --workspace ci-results --bundle-id "pr-42-run-1"| Variable | Default | Description |
|---|---|---|
AUTH_SECRET |
evidence-browser-default-secret-change-me |
Session signing secret (must be explicitly set in production) |
AUTH_BYPASS |
false |
Set to true only for trusted local or intranet deployments. All requests run as an admin user, /setup is skipped, and no login/API key is required. Do not expose an instance with this enabled to an untrusted network. |
AUTH_LOCAL_ENABLED |
true |
Enables built-in username/password login. Set to false only when OIDC_ENABLED=true; API keys and active OIDC sessions still work. |
OIDC_ENABLED |
false |
Enables OIDC authorization-code login. Requires issuer, client ID, client secret, and redirect URI. |
OIDC_ISSUER |
— | OIDC issuer URL used for discovery, for example https://auth.example.com/application/o/evidence-browser/ for Authentik. |
OIDC_CLIENT_ID |
— | Client ID from the OIDC provider. |
OIDC_CLIENT_SECRET |
— | Client secret from the OIDC provider. |
OIDC_REDIRECT_URI |
— | Public callback URL, for example https://evidence.example.com/api/auth/oidc/callback. |
OIDC_SCOPES |
openid profile email |
Space-separated scopes requested during authorization. Include a provider-specific groups scope only when required. |
OIDC_GROUPS_CLAIM |
groups |
Claim name read as the group list. |
OIDC_ADMIN_GROUP |
— | Group whose members are synced as Evidence Browser admins. Other OIDC users are synced as user. |
OIDC_ALLOWED_GROUPS |
— | Optional comma-separated allowlist. When set, users outside every listed group are rejected. |
OIDC_AUTO_PROVISION |
true |
Creates a passwordless local user on first successful OIDC login. |
OIDC_LINK_BY_VERIFIED_EMAIL |
false |
Links a verified OIDC email to an existing local user when no provider identity exists. |
OIDC_BUTTON_LABEL |
Sign in with SSO |
Login page label for the OIDC button. |
Manual sign-off path recorded for issue #164 against Authentik 2026.8.0.
The automated CI-compatible OIDC flow uses Dex in
packages/web/e2e-oidc/oidc-flow.spec.ts; Authentik remains the first target
IdP for operator setup and manual provider sign-off.
- In Authentik, create a Property Mapping for the admin group claim if the default
groupsclaim is not already suitable:- Scope name:
groups - Expression:
return [group.name for group in request.user.ak_groups.all()]
- Scope name:
- Create an OAuth2/OpenID Provider:
- Authorization flow: the instance default explicit or implicit consent flow.
- Client type: Confidential.
- Redirect URI:
https://<evidence-browser-host>/api/auth/oidc/callback. - Signing key: select the instance default.
- Scopes: include
openid,profile,email, and thegroupsmapping.
- Create an Application and attach the provider.
- Assign the Application to the users or groups allowed to sign in.
- Configure Evidence Browser:
AUTH_SECRET="$(openssl rand -base64 32)" AUTH_LOCAL_ENABLED=false OIDC_ENABLED=true OIDC_ISSUER=https://<authentik-host>/application/o/<slug>/ OIDC_CLIENT_ID=<provider-client-id> OIDC_CLIENT_SECRET=<provider-client-secret> OIDC_REDIRECT_URI=https://<evidence-browser-host>/api/auth/oidc/callback OIDC_GROUPS_CLAIM=groups OIDC_ADMIN_GROUP=evidence-browser-admins OIDC_ALLOWED_GROUPS=evidence-browser-users,evidence-browser-admins
- Verify that an
evidence-browser-adminsmember lands in/admin, and that a user outsideOIDC_ALLOWED_GROUPSis rejected with the safe login error.
The regression suite also covers the local-login off switch: with
AUTH_LOCAL_ENABLED=false, POST /api/auth/login returns 403, the login page
hides username/password fields, OIDC can still issue the normal
evidence_session cookie, and API keys remain valid.
Upgrade note: migration v1 rebuilds the users table. Back up evidence.db before upgrading an existing instance.
| Variable | Default | Description |
|---|---|---|
STORAGE_TYPE |
local |
local or s3 |
STORAGE_LOCAL_PATH |
— | Directory path (required when local) |
S3_BUCKET |
— | Bucket name (required when s3) |
S3_REGION |
auto |
AWS region or auto for R2 |
S3_ENDPOINT |
— | Custom endpoint (e.g. R2: https://<account>.r2.cloudflarestorage.com) |
S3_ACCESS_KEY_ID |
— | S3 access key |
S3_SECRET_ACCESS_KEY |
— | S3 secret key |
S3_FORCE_PATH_STYLE |
false |
Use path-style URLs (for MinIO, etc.) |
| Variable | Default | Description |
|---|---|---|
MAX_BUNDLE_SIZE |
524288000 (500 MB) |
Maximum zip file size |
MAX_FILE_COUNT |
10000 |
Maximum files per bundle |
MAX_SINGLE_FILE_SIZE |
104857600 (100 MB) |
Maximum single file size |
CACHE_TTL_MS |
1800000 (30 min) |
In-memory cache TTL |
CACHE_MAX_ENTRIES |
50 |
LRU cache capacity |
| Variable | Default | Description |
|---|---|---|
MCP_API_KEY |
— | Optional read-only Bearer token for /api/mcp instance auth. Scoped eb_ API keys are also accepted. If unset, only informational tools are public unless AUTH_BYPASS=true. |
A bundle is a zip file with a required manifest.json:
my-bundle.zip
├── manifest.json
├── index.md # landing page (referenced by manifest)
├── logs/
│ └── output.log
└── screenshots/
└── step-1.png
{
"version": 1,
"title": "PR #42 — Test Results",
"index": "index.md"
}| Field | Type | Required | Description |
|---|---|---|---|
version |
number | yes | Bundle format version (use 1) |
title |
string | yes | Displayed as the page title |
index |
string | yes | Relative path to the landing file |
Additional fields are allowed and passed through.
Derived from the zip filename (without .zip). Supports hierarchical paths:
org/repo/pr-42/run-1 → stored as org/repo/pr-42/run-1.zip
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=./data/bundlesPlace bundles at {STORAGE_LOCAL_PATH}/{bundleId}.zip.
STORAGE_TYPE=s3
S3_BUCKET=evidence-bundles
S3_REGION=auto
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...Upload bundles with key {bundleId}.zip.
GET /llm.txt
Returns a plain-text guide describing the bundle format, storage configuration, upload instructions, size limits, and available API endpoints. Designed for LLM consumption (similar to robots.txt).
Evidence Browser exposes an MCP server via Streamable HTTP:
POST /api/mcp
Accept: application/json, text/event-stream
Authorization: Bearer <MCP_API_KEY> # only if MCP_API_KEY is set
The endpoint also accepts Authorization: Bearer <eb_...> scoped API keys. Read tools
require a read, upload, or admin scoped API key; create_upload_url requires
upload or admin. MCP_API_KEY is read-only instance access and cannot mint upload
URLs. If MCP_API_KEY is unset, unauthenticated callers can use informational tools only.
Available tools:
| Tool | Description |
|---|---|
get_bundle_schema |
Returns manifest.json schema and zip structure |
get_storage_info |
Returns storage type, bucket, endpoint, region (no secrets) |
get_upload_instructions |
Step-by-step upload instructions for the current storage |
list_workspaces |
Lists available workspaces |
list_bundles |
Lists bundles in a workspace, optionally filtered by uploader, time range, and limit |
create_upload_url |
Mints a short-lived signed multipart upload URL for upload/admin scoped callers |
get_bundle_overview |
Returns bundle metadata, manifest, file tree, and inline index file content |
get_bundle_tree |
Returns the file tree for one bundle |
read_bundle_file |
Reads one text file up to 256 KB; binary or oversized files return metadata plus a web URL |
Test with MCP Inspector:
npx @modelcontextprotocol/inspector http://localhost:3000/api/mcpAll endpoints require authentication unless AUTH_BYPASS=true is explicitly enabled. AUTH_BYPASS=true is intended only for trusted local or intranet deployments: it skips /setup, treats every request as admin, and logs a startup warning. Do not expose a bypass-enabled instance to an untrusted network.
Bundle routes are scoped to a workspace. Use the workspace slug for {ws}; {bundleId} is a single URL path segment, so encode reserved characters, including slashes as %2F, when constructing URLs.
| Method | Path | Description |
|---|---|---|
GET |
/w/{ws} |
Workspace bundle list page |
GET |
/w/{ws}/b/{bundleId} |
Bundle landing page |
GET |
/w/{ws}/b/{bundleId}/f?path={filePath} |
File viewer |
GET |
/api/w |
List workspaces (JSON) |
GET |
/api/w/{ws} |
Workspace details (JSON) |
GET |
/api/w/{ws}/bundle |
List workspace bundles (JSON) |
POST |
/api/w/{ws}/bundle |
Upload a bundle |
GET |
/api/w/{ws}/bundles/{bundleId}/meta |
Bundle manifest + file tree (JSON) |
GET |
/api/w/{ws}/bundles/{bundleId}/tree |
File tree only (JSON) |
GET |
/api/w/{ws}/bundles/{bundleId}/file?path={filePath} |
Raw file content |
GET |
/api/w/{ws}/bundles/{bundleId}/preview?path={htmlFilePath} |
Sandboxed HTML preview |
GET |
/llm.txt |
LLM integration guide (plain text) |
POST |
/api/mcp |
MCP Streamable HTTP endpoint |
AUTH_SECRET="$(openssl rand -base64 32)" docker compose upThe compose file builds the local Docker image, maps 3000:3000, and stores
bundles under ./data on the host. AUTH_SECRET is required; generate a fresh
random value instead of committing or reusing a fixed value.
For a local Node production build without Docker:
npm run build
npm run startnpm run build compiles:
packages/shared(shared types/utilities)packages/api(Hono API server)packages/web(Vite SPA), then copies it to rootweb/for static serving by the API runtime
- Hono (Node server + API routes)
- React 19
- Vite 8 (web app build/dev server)
- Tailwind CSS 4
- Shiki (Syntax highlighting)
- MCP SDK (AI agent integration)
This project is licensed under the MIT License. See LICENSE for details.

