Questions Worth Asking Continuously — a web application for exploring survey instruments (questions and answer options) from previously conducted studies. Built with SvelteKit and backed by qwacback.
- SvelteKit (static adapter, SPA mode) with Svelte 5
- qwacback — Go/PocketBase backend providing the REST API
- @correlaid/cdl-design — Civic Data Lab design system (tokens, components)
- Fuse.js — client-side fuzzy search
- Zod — input validation
- Bun — package manager
bun install
bun run devRequires a running qwacback instance. Set the URL in .env:
cp .env.example .env
# edit PUBLIC_POCKETBASE_URL to point to your backend| Variable | Required | Description |
|---|---|---|
PUBLIC_POCKETBASE_URL |
Yes | URL of the qwacback/PocketBase backend |
| Command | Description |
|---|---|
bun run dev |
Start dev server |
bun run build |
Build for production |
bun run preview |
Preview production build |
bun run check |
Type-check the project |
bun run lint |
Run linting |
bun run format |
Format code with Prettier |
bun run test |
Run all tests |
bun run test:e2e |
Run the end-to-end tests |
bun run test runs two Vitest projects:
- server (Node): unit tests,
src/**/*.test.tsandserve.test.js - client (Chromium via Playwright): component tests,
src/**/*.svelte.test.ts
The component tests need Playwright's Chromium once:
bunx playwright install chromium-headless-shellWhere Playwright can't install its browser (e.g. an unsupported Linux distribution), use a local Chromium instead:
PLAYWRIGHT_CHROMIUM_PATH=/usr/bin/chromium bun run testRun one project with bun run test -- --project server (or client).
bun run test:e2e runs the Playwright tests in e2e/. It builds the app, serves it with serve.js on port 4173 and answers the API calls from the fixtures in e2e/backend.ts, so no qwacback is needed. PLAYWRIGHT_CHROMIUM_PATH works here too.
The app is a fully static SPA served by serve.js. All data comes from the qwacback API:
GET /api/questions— list of all questions (home page)GET /api/questions/{id}— full question detail including variables, group, and study (question detail page)GET /api/questions/{id}/xml— DDI XML exportGET /api/questions/{id}/xlsform— XLSForm JSONGET /api/studies/{id}/questions— questions for a studyGET /api/studies/{id}/export— DDI XML export of a full study
The About and Imprint texts are HTML snippets from the cdl-wp-eins repository (cdl-content.ts). They're fetched at build time from the commit pinned in CDL_CONTENT_REF, sanitised to text and links, and bundled into the app. To publish new texts, bump CDL_CONTENT_REF. In CI (CI set) a missing snippet fails the build, since the Imprint is required content.
Built and started with Bun (nixpacks.toml): bun install --frozen-lockfile, bun run build, bun serve.js. bun.lock is the only lockfile.
serve.js is a small static file server for build/:
- Security headers (CSP,
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy) come fromsecurity-headers.js. The Vite dev and preview servers use the same file, so all three send the same headers.PUBLIC_POCKETBASE_URLis read at runtime for the CSP'sconnect-srcandimg-src. - Caching:
/_app/immutable/*getsCache-Control: public, max-age=31536000, immutable; HTML getsno-cache; other files one hour. - Compression: the adapter writes
.brand.gznext to each file (precompress), andserve.jssends them to clients that accept them. - SPA fallback: unknown page routes get
index.html; missing assets get a 404. - Health checks at
/healthand/healthz.