- JavaScript 95.8%
- HTML 1.9%
- CSS 1.2%
- Dockerfile 1.1%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| api | ||
| documentation | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .env.test.example | ||
| .gitignore | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE.txt | ||
| README.md | ||
Wisp
Wisp is a character-backstory ideation tool: it walks you through a branching series of choices (Setting → Tone → Background → Defining Event → Goal → Relationship → Optional Traits) and suggests options that fit — or deliberately don't — based on what you've already picked. Nothing is ever locked in or filtered out; suggestions are inspiration, not restrictions.
Full design rationale lives in documentation/specs.md. Build sequencing and current progress live in documentation/plan.md.
Forgejo Actions automatically build and number the latest updates on the main branch to the Package section of the repository.
How it works
Each category (e.g. Tone) is scored against everything you've already chosen: every stored connection between an option and one of your prior picks contributes a weight (-5 to +5) to that option's affinity score, summed across all your choices so far. The top 8 highest-scoring options are highlighted as good fits, and the bottom 2 lowest-scoring are highlighted as an easy on-ramp to a genuine pivot — everything else stays visible, just unhighlighted. Optional Traits (Quirks, Motivations, Flaws, Secrets, Values) are pure inspiration and are never scored.
Repo layout
api/ Express API + Postgres schema, seed data, and CLI tools (see api/README's-worth of detail below)
src/app.js Assembles the Express app (middleware + routes + static-serving hook)
src/index.js Entrypoint: createApp() + listen()
src/config.js Env vars -> config object
src/logger.js Shared pino instance
src/middleware/ Request logging, etc.
src/routes/ One file per route group (health, options)
src/staticWeb.js Serves web/dist (production single-container mode only — see Dockerfile)
web/ React frontend (Vite + Tailwind) — full wizard flow through Setting → ... → Optional Traits → Core Concept (see documentation/plan.md Phase 4; no automated frontend tests yet)
documentation/ specs.md (design) and plan.md (build plan / progress)
docker-compose.yml Dev: db + api as separate services
docker-compose.prod.yml Prod: single app container (api + built web) + db — see Dockerfile
Dockerfile Multi-stage prod build: builds web/, copies dist into the api image
.forgejo/workflows/build.yml CI: builds + pushes the app image to the Forgejo registry on push to main
Getting started (local dev)
Just want to run Wisp, not hack on it? Skip this section — the Deployment section below pulls a pre-built image from the container registry, no source tree or
npm installrequired.
-
Copy the env files and fill in secrets:
cp .env.example .env cp .env.test.example .env.test.env.testis used only bynpm test— it points at a separate, test-only database on the same Postgres instance, created automatically on first run. -
Start Postgres:
docker compose up -d -
Install API dependencies and run migrations:
cd api npm install npm run migrate -
Seed the database with the full option/connection dataset (idempotent — safe to re-run):
npm run seed -
Start the API:
npm start -
Start the frontend (separate terminal):
cd web npm install npm run dev
Deployment
Production packaging deliberately diverges from dev: instead of separate db/api/web services, it's a single app container (API + the built frontend, served via express.static, see Dockerfile and api/src/staticWeb.js) plus db — 2 containers, not 3. The app image is released through a Forgejo container registry rather than built on the deploy host, so the host only ever needs docker-compose.prod.yml + .env + registry access — never the source tree.
1. CI: image build & push (automatic)
On every push to main, .forgejo/workflows/build.yml builds the root Dockerfile and pushes it to the Forgejo container registry at code.qajohnson.me/q-johnson/wisp-app, tagged with both latest and the git short SHA. Past versions stay pullable this way instead of being silently overwritten on every release — that's what makes rollback (below) possible. No manual build/push step is needed for a normal release; just merge to main.
If you ever do need to build and push by hand (e.g. from a machine without CI access), with a registry login (docker login code.qajohnson.me):
SHA=$(git rev-parse --short HEAD)
docker build -t code.qajohnson.me/q-johnson/wisp-app:$SHA \
-t code.qajohnson.me/q-johnson/wisp-app:latest .
docker push code.qajohnson.me/q-johnson/wisp-app:$SHA
docker push code.qajohnson.me/q-johnson/wisp-app:latest
2. Target host setup (first time only)
The deploy host needs nothing from this repo except two files and Docker itself:
mkdir wisp && cd wisp
# copy these two files from the repo onto the host:
# docker-compose.prod.yml
# .env.example → fill in and save as .env
Fill in .env on the host (see Environment variables below) — in particular, set real values for POSTGRES_PASSWORD and CORS_ORIGINS (the public domain(s) the frontend will be served from). Then log in to the registry once so docker compose pull can fetch the image:
docker login code.qajohnson.me
3. Deploy / update
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
This starts (or recreates) wisp-db and wisp-app. On container start, wisp-app runs npm run initialize (migrations, then seed) before starting the server — both steps are idempotent, so re-running this on every deploy is safe even when the schema/seed data hasn't changed. Re-run these same two commands for every subsequent update; up -d only recreates containers whose image/config actually changed.
Check it came up healthy:
docker compose -f docker-compose.prod.yml ps
curl http://localhost:$API_PORT/health
4. Reverse proxy
wisp-app listens on plain HTTP on API_PORT (3743 by default) and expects to sit behind a reverse proxy (e.g. Yacht, Caddy, nginx, Traefik) that terminates TLS and forwards to it — it does not handle HTTPS itself. Point your proxy's public domain at http://<host>:${API_PORT}, and make sure that same domain (with scheme, e.g. https://wisp.example.com) is listed in CORS_ORIGINS in .env, since the API validates CORS against an explicit allow-list rather than a wildcard.
5. Roll back
Temporarily point app.image in docker-compose.prod.yml at a specific SHA tag instead of latest:
app:
image: code.qajohnson.me/q-johnson/wisp-app:<short-sha>
then:
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
Revert the file back to :latest once a fixed version is released.
Environment variables
Set in .env on both dev and production hosts (see .env.example):
| Variable | Purpose |
|---|---|
POSTGRES_USER |
Postgres role used by both db and the app's DATABASE_URL |
POSTGRES_PASSWORD |
Postgres password — set a real secret in production, never commit it |
POSTGRES_DB |
Database name created on db's first boot |
POSTGRES_PORT |
Host port db binds to (dev only — dropped from prod's port mapping since only app needs to reach db, over the Compose network) |
DATABASE_URL |
Full Postgres connection string the API uses — points at db:5432 in Compose, localhost:$POSTGRES_PORT if running the API directly on the host |
API_PORT |
Port the API (and, in production, the bundled frontend) listens on |
CORS_ORIGINS |
Comma-separated allow-list of origins permitted to call the API — set to the frontend's real domain(s) in production, http://localhost:5173 (Vite) in dev |
.env.test (see .env.test.example) is separate and only used by npm test — it's not part of deployment.
Useful commands (run from api/)
| Command | What it does |
|---|---|
npm start |
Runs the API (GET /health, GET /options) |
npm run migrate |
Applies Postgres schema migrations |
npm run seed |
Loads/updates all option + connection CSVs (seed/csv/) into the DB |
npm run initialize |
migrate + seed in one step — what the Docker image runs on container start |
npm run affinity |
Interactive CLI walkthrough of the whole choice flow, scoring against your picks as you go — useful for testing without a frontend |
npm run affinity -- <category> [selected ids or names...] |
One-off affinity check for a single category |
npm run affinity -- --list <category> |
Lists a category's options with ids, for reference |
npm test |
Runs the automated test suite against the separate test DB (.env.test) — resets/reseeds fixtures before each test |
Useful commands (run from web/)
| Command | What it does |
|---|---|
npm run dev |
Starts the Vite dev server |
npm run build |
Production build (web/dist) |
npm run preview |
Serves the production build locally |
npm run lint |
Runs Oxlint |
Tech stack
- API: Node.js (ESM), Express,
pg(no ORM — hand-written SQL),pinofor structured logging,node-pg-migratefor schema migrations. - Database: PostgreSQL. Schema: an
optionstable (one row per choice, tagged by category) and anoption_connectionstable (weighted, undirected pairs between options) — seedocumentation/specs.mdfor the full data model. - Frontend: React 19 + Vite + Tailwind CSS 4 — see
documentation/plan.mdPhase 4.