Survey engine for TTRPGs
  • JavaScript 52.7%
  • Go 31.4%
  • CSS 14.1%
  • Nix 0.5%
  • HTML 0.5%
  • Other 0.8%
Find a file
2026-11-02 21:53:57 -08:00
cmd/lantern Add individual survey response deletion and correct module path 2026-11-02 21:53:57 -08:00
db/queries Add individual survey response deletion and correct module path 2026-11-02 21:53:57 -08:00
internal Add individual survey response deletion and correct module path 2026-11-02 21:53:57 -08:00
k8s Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
web Add individual survey response deletion and correct module path 2026-11-02 21:53:57 -08:00
.dockerignore Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
.env.example Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
.gitignore Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
devenv.lock Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
devenv.nix Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
devenv.yaml Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
Dockerfile Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
go.mod Add individual survey response deletion and correct module path 2026-11-02 21:53:57 -08:00
go.sum Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
Makefile Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00
README.md Add individual survey response deletion and correct module path 2026-11-02 21:53:57 -08:00
sqlc.yaml Initial commit: Lantern survey application 2026-11-02 14:20:36 -08:00

Lantern

A branching survey application for TTRPG groups. Go + Fiber v3, PostgreSQL, sqlc, Goose, and native web components. Frontend assets and migrations are embedded in one binary; PostgreSQL runs separately.

Local development

Install devenv with Nix, then:

cp .env.example .env
# Add your Google OAuth credentials to .env.
devenv up

Open http://localhost:8080. devenv up starts PostgreSQL 16 on loopback port 55432, waits for it to become healthy, and starts the app. Migrations run on app startup. Stop the foreground process with Ctrl-C, or use devenv down after starting with devenv up -d.

The exact Go, Bun, sqlc, Goose, PostgreSQL, and Prettier packages are pinned by devenv.lock. Go's automatic toolchain download is disabled. The browser runs native ES modules: there is no frontend build or npm install. Bun runs the frontend unit tests; Prettier is provided directly by Nix.

To enter the tool environment:

devenv shell
make generate           # regenerate Go queries after editing SQL
make format             # gofmt and Prettier
make check              # go vet, sqlc vet, and frontend formatting check
make test               # Go race tests and Bun frontend tests
make test-integration   # needs the devenv PostgreSQL service running
make build              # bin/lantern, with assets and migrations embedded

After changing Go or browser code, restart the server process to rebuild its embedded assets: devenv processes restart server. There is no live-reload server or authentication bypass.

Google sign-in

Create a Google OAuth client of type Web application. Register:

  • Local redirect: http://localhost:8080/auth/google/callback
  • Production redirect: https://YOUR_HOST/auth/google/callback

Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .env locally and your secret manager in production. Configure Google's consent screen/audience for the intended users; a Google app in testing mode may restrict access to test accounts. Lantern itself allows any successfully authenticated Google account to create surveys.

Only the stable Google subject identifier is stored for creators, not a password, profile, email address, or Google access token. Without credentials, the landing page is usable but creator sign-in is unavailable.

Color scheme

Light evokes a warm day with cream surfaces, golden yellow highlights, and burnt-orange accents. Dark evokes night with deep navy surfaces, cool blues, and aqua accents. The palette applies to controls, cards, results, and the landing illustration, not just the page background.

Click the header's sun/moon icon to reveal System, Light, and Dark choices; the menu stays hidden otherwise. It closes after a selection, when clicking outside it, or with Escape, and supports keyboard navigation. The icon reflects the current light/dark appearance, including when following the system. The control is available to creators and anonymous respondents. System is the default and follows OS color-scheme changes immediately. Explicit choices persist in browser-local storage and are shared across tabs, not stored on a creator account. If storage is unavailable, switching still works for the current page.

The preference is applied before styles load to avoid a light/dark flash. Changing it never reloads the page or discards draft edits or survey answers.

Project layout

cmd/lantern/             startup, graceful shutdown, JSON slog configuration
internal/config/         environment parsing and validation
internal/httpapp/        Fiber routes, auth, creator and respondent handlers
internal/domain/         survey graph and answer validation; no HTTP or SQL
internal/db/             sqlc-generated types and queries — do not edit by hand
db/queries/              SQL query source of truth
internal/migrations/     embedded, versioned Goose migrations and runner
web/app.js               component registration only
web/core.js              shared API, HTML escaping, formatting helpers
web/theme.js             early browser-local color-scheme preference
web/theme.css            semantic light/dark palette for all components
web/autosave.js          debounced, serialized draft persistence
web/draft-questions.js   question-editing operations for incomplete drafts
web/survey-api.js        separate real-response and preview API adapters
web/survey.js            pure browser-side path and answer checks
web/components/          focused native components and view templates
web/tests/               Bun unit tests
k8s/                     example Kustomize base for GitOps overlays

Components use light DOM intentionally, so shared semantic controls and styling stay simple. HTML templates escape user-supplied values with esc(). Split stateful behavior from larger rendering templates rather than growing a single application script. Comments explain module responsibilities and non-obvious invariants; generated code stays untouched.

Database development

All application queries live in db/queries/; sqlc.yaml generates typed Go methods in internal/db/. Schema changes are versioned Goose SQL migrations, which sqlc also reads as its schema source. Do not put ad-hoc SQL in handlers.

Inside devenv shell:

goose -dir internal/migrations/sql create describe_change sql
# Edit the new migration, then update queries as needed.
make generate
make migrate
make migration-status

The embedded Goose runner uses a PostgreSQL advisory lock, so replicas can start concurrently. The runtime database role needs permission to apply migrations. Existing versioned migrations must not be edited once applied; add a new migration instead. Database backups remain the operator's responsibility, including recovery of soft-deleted data.

Survey behavior

  • Draft edits autosave after a short pause (400 ms). A visible status confirms when changes are saved; failed saves keep the local edits and offer a retry. Pending saves are drained before preview, publication, duplication, and normal in-app navigation. Unsaved changes still trigger a browser warning on reload or leaving the page. Wait for All changes saved before switching devices.
  • Incomplete or invalid definitions can be saved and reopened on another device. Preview and publication validate the definition and show errors to fix first.
  • Preview draft is creator-only and available only while a survey is a draft. It uses the saved draft and the real branching/review UI. Submit preview validates answers but never inserts a response, changes survey status, creates a duplicate, or issues a respondent cookie. Test runs can be repeated freely.
  • Publishing validates and freezes the question definition permanently.
  • Supported answers: single choice, multiple choice, free text, numbers, and individual ratings. There are no file uploads.
  • Single- and multiple-choice answers can have an optional plain-text description. Nonblank descriptions appear beneath their labels in surveys and draft previews; missing, empty, or whitespace-only descriptions are hidden. Descriptions autosave with the draft and are preserved by duplication and publication.
  • Only single-choice options branch. An option's destination overrides the question's default destination. Otherwise, questions advance in order.
  • Destinations point forward, so paths can split and rejoin without cycles. End survey finishes a path explicitly.
  • Respondents can go back and review. Inactive branch answers stay only in page memory in case they switch back. The server independently follows the published path and discards inactive answers on final submission.
  • There is no save/resume, submission editing, or respondent account.
  • A random, HttpOnly, per-survey cookie limits submission to once per browser. The database uniqueness constraint also prevents concurrent double submits. Clearing cookies or using another browser bypasses this by design. Players sharing the same browser also share that response limit.
  • Only the owning creator can read aggregate results and individual anonymous submissions. Multiple-choice percentages use people who answered that question as the denominator, so percentages can total more than 100%.
  • Expand an individual response and choose Delete response to remove it after confirmation. Only the survey owner can do this, on open or closed surveys. Responses are soft-deleted and excluded from results, summaries, and all response counts; there is no restore UI or API. If the survey is still open, the same browser can submit a replacement response.
  • Closed surveys cannot reopen. Duplication creates an editable draft without copying responses. Deletion atomically soft-deletes the survey and responses; there is no restore UI or API.

No respondent name, email, IP, or user agent is stored. Free text can still contain identifying information if a respondent enters it. Private URLs are bearer links: anyone receiving one can view and answer the survey. No answer or private-link request logging is enabled; review ingress logs separately.

Production

Required configuration:

Variable Purpose
DATABASE_URL PostgreSQL DSN; use verified TLS as appropriate for your cluster
BASE_URL Exact public HTTPS origin, without a path
GOOGLE_CLIENT_ID Google OAuth client ID
GOOGLE_CLIENT_SECRET Google OAuth client secret
LISTEN_ADDR Optional; defaults to :8080

Mutation requests must have an Origin matching BASE_URL. Sessions are shared in PostgreSQL; no sticky sessions or writable application volume are needed. Health endpoints: /healthz for liveness and /readyz for database readiness. Logs use log/slog JSON on stdout, including Goose migrations.

docker build -t YOUR_REGISTRY/lantern:YOUR_TAG .
kubectl kustomize k8s > rendered.yaml

The Kubernetes files are examples, not cluster-specific production values. Patch the image (preferably to a digest), public origin, ingress host/class, and TLS secret in a GitOps overlay. Provision lantern-secrets with the three secret variables above; no plaintext production Secret is checked in. The base uses a non-root container, read-only filesystem, dropped capabilities, resource bounds, and startup/readiness/liveness probes. PostgreSQL is external.

Verification and current limits

Tests cover graph validation, branch pruning/rejoining, required answers, choice and numeric validation, ownership isolation, immutable publication, cookie replay protection, closing, duplication, and soft deletion. Individual response deletion tests cover ownership and survey isolation, result/count filtering, replacement submissions, confirmation, failures, and summary updates. Preview tests also cover ownership, draft-only access, validation, repeated submissions with no stored responses/cookies, and no survey copies. Autosave tests cover coalescing edits, request ordering, mid-flight changes, and failed-save recovery. Integration tests use generated queries and run against real PostgreSQL when TEST_DATABASE_URL is set; use a disposable development/test database.

A headless Chromium smoke test also exercised draft editing/saving, branch switching with retained answers, review, submission, cookie replay prevention, and creator results without browser exceptions. A further browser run verified incomplete-draft persistence across reload, validation gates, repeated preview submissions with no cookies/copies/responses, offline recovery, slow-save ordering, navigation flushes, and a real response after preview. The built container was smoke-tested as non-root with a read-only filesystem against local PostgreSQL; the Kustomize base renders successfully. It has not been deployed to a cluster.

Google's live OAuth flow requires your credentials and has not been verified against an actual Google account here. This initial implementation does not include exports, a restore interface, ranking questions, survey templates, or a visual node-graph editor. Branches are configured through destination selectors in the visual question builder.