English · 中文
A fast, lightweight, security- and privacy-first API documentation portal.
Caution
Applicability — read first. api_show is built for high-confidentiality, coarse-grained-permission environments:
- ✅ Fits: internal / LAN-only API documentation that should never reach the public internet, where access control is expressed at project + tag-prefix granularity (e.g. "Alice can see project
payments, tagsauth*andusers*"). - ❌ Does NOT fit: SaaS-style multi-tenant docs portals, per-endpoint / per-field ACL, per-operation rate limiting, audit logging requirements, or any setup that needs to be exposed to the open internet.
If you need fine-grained operation-level permissions, audit trails, SSO/OAuth federation, or public hosting — pick a different tool. api_show deliberately trades feature breadth for a small, auditable surface that fits inside a LAN.
api_show aggregates OpenAPI specs from multiple producer repos into a
single portal rendered by an embedded Redoc bundle (served locally — no
CDN dependency). Each producer repo posts its docs/openapi.yaml on tag
release; viewers open the same URL, pick project + version from a
dropdown, and see the rendered API. Spec updates do not require
recompiling the portal.
| Sign in | Bind TOTP (one-time) |
|---|---|
![]() |
![]() |
Portal — demo_a |
Portal — demo_b |
|---|---|
![]() |
![]() |
The QR code and base32 secret in the bind screenshot are intentionally blurred / redacted — each first-time login generates a fresh secret.
Dark theme tokens match Redoc's left sidebar + right code panel. Schema
labels and table rows lift contrast via webroot/style.css overrides
scoped to #api-reference.
Threat model — LAN only. No public IP, never exposed to the internet. When
only_local = true(default) any non-private / non-loopback IP hitting/uploadis rejected at the application layer regardless of the Bearer token.Auth — three modes, picked via the
auth_modefield in TOML config:
Mode Behaviour pwdusername + password only. No TOTP. Best for IDE breakpoint debugging. pwd_totpDefault. Verify password; if the user has TOTP bound, require a 6-digit code as a 2nd factor. Otherwise issue a session straight away. totpNo password. TOTP is the credential; first login auto-enrols via /bind-totp.Passwords are never stored in TOML. The argon2id hash lives in an AES-GCM-encrypted sidecar blob under
<data_dir>/pwd_blobs/. The admin CLI lets you create users without ever knowing their password — see Adding a user below.
api_show sits in a narrow niche: multi-spec aggregation behind a firewall, with per-user spec rewriting, no SaaS dependency, single-binary deploy. Most adjacent tools either drop one of those properties or are full SaaS platforms.
Legend: ✅ supported ·
| Capability | api_show | Swagger UI / Redoc (raw) | Redocly CLI | Stoplight Elements | Stoplight Platform / Bump.sh | ReadMe.com | Mintlify | Backstage TechDocs |
|---|---|---|---|---|---|---|---|---|
| Aggregate multiple OpenAPI specs in one portal | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ||
| Self-hosted single binary, no runtime deps | ✅ | ✅ | ❌ (SaaS) | ❌ (SaaS) | ❌ (SaaS) | ❌ (heavy platform) | ||
| LAN-only enforced at app layer (private/loopback IP gate) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| Built-in multi-user auth (pwd / TOTP / 2FA) | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | |
| Per-user ACL at project + tag-prefix level | ✅ | ❌ | ❌ | ❌ | ❌ | |||
| Server-side spec rewrite drops disallowed ops before delivery | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Producer upload endpoint (CI POSTs spec on tag) | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | |
| No CDN / no external font fetch at runtime | ✅ | ❌ | ❌ | ❌ | ||||
| Passwords never stored in config, argon2id PHC in AES-GCM blob | ✅ | — | — | — | ✅ | ✅ | ✅ | |
Admin CLI never sees plaintext password (/set-password flow) |
✅ | — | — | — | ||||
| Spec updates without recompiling / redeploying the portal | ✅ | ❌ (rebuild) | ✅ | ✅ | ❌ (rebuild) | ❌ (rebuild) | ||
| Audit log / SSO / fine-grained per-endpoint ACL | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | |
| Public-internet hosting is a supported deployment | ❌ (intentional) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Pick api_show when all of these are true:
- Multiple internal teams ship OpenAPI specs and you want one portal listing them.
- Access stays inside a LAN / VPN; you do not want to depend on a SaaS or expose docs to the public internet.
- Coarse access control (project + tag-prefix per user) is enough; you do not need per-endpoint or per-field ACL or audit trails.
- You want a single Go binary with embedded Redoc — no Node toolchain, no build pipeline, no CDN call at runtime.
Pick something else when you need any of: per-endpoint ACL, audit logging, SSO/OIDC federation, public hosting, fine-grained quota / rate limiting, or a polished docs CMS with custom marketing pages.
# 1. Download a release archive
wget https://github.com/0xYeah/api_show/releases/download/<VERSION>/api_show_linux_release_<VERSION>.zip
unzip api_show_linux_release_<VERSION>.zip && cd api_show
# 2. One-shot install (creates /api_show/{api_show, conf/, logs/}, installs unit file)
sudo ./install_api_show.sh
# 3. Start → edit config → restart
sudo systemctl start api_show
sudo $EDITOR /api_show/conf/api_show_config.toml # fill secret + upload_token
sudo systemctl restart api_show
sudo systemctl status api_showinstall_api_show.sh actions:
| Action | Behaviour |
|---|---|
install (default) |
First install. Existing binary + config get a _YYYY-MM-DD_HH-MM-SS backup suffix. |
update |
Stop service → replace binary (preserve data/ + conf/) → restart. |
uninstall |
Stop service + delete unit file + delete /api_show/ directory (irreversible). |
Path: /api_show/conf/api_show_config.toml (auto-created on first start with
placeholder values).
# HMAC cookie key + per-blob key derivation salt. >= 16 bytes, ASCII.
secret = "REPLACE_ME_at_least_16_chars"
# Bearer token /upload accepts.
upload_token = "REPLACE_ME_bearer_token"
# Root dir for uploaded openapi specs + totp_blobs/ + pwd_blobs/.
data_dir = "./data"
# HTTP listen port. Bound to IPv4 0.0.0.0 only.
port = 12110
# LAN-only mode. true = /upload rejects any non-private / non-loopback IP
# at the app layer, independent of Bearer correctness.
only_local = true
# Auth policy: pwd | pwd_totp (default) | totp
auth_mode = "pwd_totp"
# User roster + per-user ACL. Edit + restart to add/remove users.
# TOML never holds passwords — they live in encrypted blobs under
# <data_dir>/pwd_blobs/. See "Adding a user".
[[users]]
username = "alice"
acl = { demo_a = ["*"], demo_b = ["auth", "users"] }
[[users]]
username = "bob"
acl = { demo_a = ["*"] }Constraints:
secretempty or under 16 bytes → process refuses to start.upload_tokenempty →/uploadreturns503 uploads disabled.auth_modenot in{pwd, pwd_totp, totp}→ process refuses to start.- No environment variables are read. Config is only sourced from TOML; deploy by overwriting the file.
- The first start writes a placeholder config when the file is missing, so startup never fails because of a missing config file.
GET /login→ enterusername+ password → submit.- Argon2id verifies → 7-day session cookie issued →
302to/.
GET /login→ enterusername+ password → submit.- Password verified → portal checks whether the user has a bound TOTP.
- Not bound → session issued directly →
302to/. - Bound →
302to/login-totp.
- Not bound → session issued directly →
/login-totp→ enter the 6-digit code → session issued →302to/.
GET /login→ enterusername, leave TOTP blank → submit.- Account not bound yet →
302to/bind-totp, scan QR + enter first code → secret persisted (encrypted) → session issued →302to/. - Subsequent logins:
username+ 6-digit code → verify → session →/.
If the user was added without -p, or reset_pwd was run against them, the
portal redirects to /set-password on the next login attempt. The user picks
a password (minimum 8 characters); the admin never sees plaintext.
After login, the root page lists allowed projects + versions in two dropdowns. The current selection is reflected in the URL, which can be shared with users who hold the same ACL.
The admin CLI never reveals plaintext passwords. Two add-user patterns are supported:
Pattern 1 — admin sets an initial password the user changes later:
sudo /api_show/api_show add_user alice -p "InitChangeMe!8" -acl "demo_a=*;demo_b=auth"
sudo systemctl restart api_show
# Communicate the initial password to Alice out-of-band (e.g. signed mail).
# She can change it any time the admin runs `reset_pwd alice` (next login → /set-password).Pattern 2 — user picks their own password on first login:
sudo /api_show/api_show add_user alice -acl "demo_a=*"
sudo systemctl restart api_show
# Tell Alice to visit /login with her username + any password.
# She'll be redirected to /set-password to pick her own.-acl syntax: proj1=tag1,tag2;proj2=*. Use * to grant every tag in a
project. Leaving -acl off writes an empty ACL block (admin can edit
[[users]] in the TOML afterwards).
The CLI appends a new [[users]] block to the live config file (preserving
your comments) and, with -p, writes the encrypted password blob.
./api_show version # build identity (commit / time / OS / Go version)
./api_show add_user <user> [-p <plaintext>] # append [[users]] block + optional pwd blob
[-acl <spec>]
./api_show reset_pwd <user> # delete pwd blob; user re-sets via /set-password
./api_show reset_totp <user> # delete TOTP blob; user re-binds on next login (totp / pwd_totp modes)The producer's release script POSTs docs/openapi.yaml to the portal only
after git push --tags succeeds. Endpoint + token come from a git-ignored
file in the producer repo (no environment variables):
# In the producer repo root
cat > .gen_docs.local <<'EOF'
DEPLOY_ADDR=http://<portal-host>:12110
DEPLOY_TOKEN=<must match api_show's upload_token>
# ALLOW_LOOPBACK=1 # uncomment ONLY for local self-test
EOFProducers ship their own gen_docs.sh (or equivalent) in each repo. A
reference implementation rejects loopback addresses (127.0.0.1, localhost,
[::1], 0.0.0.0) unless ALLOW_LOOPBACK=1 is set for local self-test.
Manual upload (local self-test):
curl -X POST \
-H "Authorization: Bearer <upload_token>" \
--data-binary @./docs/openapi.yaml \
"http://127.0.0.1:12110/upload?project=<proj_id>&version=v0.0.1&latest=true"| Param | Meaning |
|---|---|
project |
Project id (used as the data directory name and matched against ACL keys). |
version |
vX.Y.Z form. |
latest |
true marks this version as the default (selected first in the dropdown). |
Roster + permissions live in TOML [[users]]:
- Each
aclmap: project_id → allowed tag prefixes. *= every tag in that project.- Project absent from the user's
acl= invisible to that user. GET /api/sources.jsonfilters out projects the caller cannot see.GET /data/<proj>/<ver>/openapi.yamlrewrites the spec to drop disallowed operations + tag declarations.
Roster changes = config edit + restart. Edit
/api_show/conf/api_show_config.toml (or use add_user / reset_pwd) then
systemctl restart api_show. No tag / release dance required.
curl http://<portal-host>:12110/healthz # 200 OK = alivesudo ./install_api_show.sh updateupdate keeps /api_show/data/ (specs + TOTP/pwd blobs) and
/api_show/conf/; only the binary is replaced.
sudo ./install_api_show.sh uninstallStops the service, removes the unit file, and deletes /api_show/ recursively
(including data — irreversible).
- LAN-only deployment by design.
only_local = truerejects external IPs inside/uploadregardless of token. - Listener is bound to
0.0.0.0(IPv4) — no IPv6 dual-stack listener. secret(≥ 16 bytes) is used for HMAC-signed cookies and AES-GCM key derivation for both pwd and TOTP blobs (per-user keys).- Passwords: argon2id (m=64 MiB, t=3, p=2) with PHC-formatted output, never stored in plaintext or in TOML.
- Cookies: HttpOnly + SameSite=Lax. Set
Secure=trueonce you front the service with TLS (e.g. nginx). - No remote write APIs other than
/upload; user roster mutations require filesystem-level access on the host.
go build ./... # debug binary
./build.sh # release tarball with version stampgo.mod requires Go 1.24+. Dependencies are pure Go.
See LICENSE.



