Development patterns, testing, and architecture for the polis webapp.
- Drift detection: Whenever you modify webapp code, check the bash CLI (
cli-bash/bin/polis) for the equivalentcmd_*function. Flag any behavioral differences. - Tests required: Add or update tests for every change. Run
go test ./...after changes. - Dependency rule: The webapp imports from
cli-go/pkg/. Never put shared logic ininternal/.
webapp/
├── cmd/server/main.go # Standalone entry point
├── cmd/polis-full/main.go # Bundled CLI+server entry point
├── internal/server/
│ ├── server.go # Server struct, config, logging
│ ├── routes.go # Route registration
│ ├── handlers.go # All HTTP handlers (~40 endpoints)
│ ├── handlers_test.go # Handler tests
│ └── server_test.go # Server/validation tests
├── internal/api/
│ ├── router.go # v1 content API routes
│ ├── handlers.go # Thin HTTP → Dispatch → JSON
│ └── middleware.go # Auth, CORS, body limits
├── internal/webui/
│ ├── assets.go # Shared embedded FS (//go:embed www/*)
│ └── www/ # SPA source (index.html, app.js, style.css)
└── internal/hosted/ # Multi-tenant service (polis.pub)
mux.HandleFunc("/api/your-endpoint", s.handleYourEndpoint)Follow the existing pattern:
- Method check
- Precondition checks (keys, config)
- Parse request body
- Business logic (use
cli-go/pkg/packages) - JSON response
async yourFeature() {
const response = await fetch('/api/your-endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ field: value })
});
// ...
}func TestHandleYourEndpoint(t *testing.T) {
s := newConfiguredServer(t)
body := jsonBody(t, map[string]string{"field": "value"})
req := httptest.NewRequest(http.MethodPost, "/api/your-endpoint", body)
w := httptest.NewRecorder()
s.handleYourEndpoint(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
}Always test: happy path, wrong HTTP method, missing preconditions, invalid input.
| Helper | Purpose |
|---|---|
newTestServer(t) |
Temp dir with required subdirectories, no keys |
newConfiguredServer(t) |
Real Ed25519 keys, config, .well-known/polis |
jsonBody(t, v) |
Marshal to *bytes.Buffer for request bodies |
Single global App object in app.js with all state and methods:
currentView— active sidebar sectionviewMode—'list'or'browser'(split-pane preview)counts— cached badge counts
Welcome Screen (no site) → Init/Link Panel
→ Dashboard Screen (site configured)
├── Sidebar navigation
├── Editor Screen (full-screen editing)
└── Snippet Screen
- Screens: Full-page views toggled via
.hiddenclass - Panels: Slide-in from right (settings, comment detail)
- Toasts:
this.showToast(message, type, duration) - Confirm:
this.showConfirmModal(title, message, callback)
The SPA has only three route shapes: /_/ (default stream), /_/pql/<sentence> (any
PQL-filtered view — what every icon button and the sentence-filter widget load), and
/_/settings. Any other /_/… path falls through to the default stream. The old v3 page
routes (/_/posts, /_/blessings, /_/social/*, …) were retired.
The authoritative routing reference is webapp/CLAUDE.md (§ SPA routes) — see it before
changing route handling.
The webapp uses a dual-theme (light/dark) system of semantic CSS custom properties
defined on [data-theme], not a fixed palette. Fonts: Inter (UI), Newsreader (serif
content), JetBrains Mono (editor). The full variable contract and design system live in
webapp/CLAUDE.md — treat it as authoritative rather than duplicating values here.
site.Validate()— check site structureLoadConfig()— read.polis/webapp-config.jsonLoadKeys()— read Ed25519 keypairLoadEnv()— search:data/.env→cwd/.env→~/.polis/.env
s.LogInfo("message: %v", arg) // Level 1
s.LogError("message: %v", err) // Level 1
s.LogDebug("message: %v", arg) // Level 2Logs to data/logs/YYYY-MM-DD.log. Thread-safe with mutex.
- Path traversal:
validatePostPath(),validateContentPath()— no.., no null bytes - Content paths restricted to:
posts/,comments/,.polis/drafts/, root.md/.html - Draft IDs sanitized with whitelist regex
{"success": true, "data": {...}}Or domain-specific shapes: {"posts": [...], "count": 5}.
# Build webapp
cd webapp && go build -o polis-server ./cmd/server
# Build bundled (CLI + server)
cd webapp && go build -o polis-full ./cmd/polis-full
# Quick dev cycle
cd webapp && go build -o polis-server ./cmd/server && ./polis-server
# If cli-go packages changed, rebuild both
cd cli-go && go build ./... && go test ./... && \
cd ../webapp && go build -o polis-server ./cmd/server && go test ./...