No description
  • JavaScript 95.8%
  • HTML 1.9%
  • CSS 1.2%
  • Dockerfile 1.1%
Find a file
Quinn Johnson 291fd72471
All checks were successful
Build and Push / build (push) Successful in 9s
update license
2026-07-29 11:28:37 -05:00
.forgejo/workflows updated action - build 2026-07-29 09:10:20 -05:00
api pre deployment with testing suite 2026-07-13 09:03:41 -05:00
documentation update documentation 2026-07-29 11:26:08 -05:00
web remove goatcounter - unwanted 2026-07-14 14:37:35 -05:00
.dockerignore pre deployment with testing suite 2026-07-13 09:03:41 -05:00
.env.example remove goatcounter - unwanted 2026-07-14 14:37:35 -05:00
.env.test.example pre deployment with testing suite 2026-07-13 09:03:41 -05:00
.gitignore init 2026-07-09 13:43:27 -05:00
docker-compose.prod.yml docker images 2026-07-14 14:48:13 -05:00
docker-compose.yml enh: frontend draft 2026-07-09 18:19:12 -05:00
Dockerfile pre deployment with testing suite 2026-07-13 09:03:41 -05:00
LICENSE.txt update license 2026-07-29 11:28:37 -05:00
README.md update documentation 2026-07-29 11:26:08 -05:00

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 install required.

  1. Copy the env files and fill in secrets:

    cp .env.example .env
    cp .env.test.example .env.test
    

    .env.test is used only by npm test — it points at a separate, test-only database on the same Postgres instance, created automatically on first run.

  2. Start Postgres:

    docker compose up -d
    
  3. Install API dependencies and run migrations:

    cd api
    npm install
    npm run migrate
    
  4. Seed the database with the full option/connection dataset (idempotent — safe to re-run):

    npm run seed
    
  5. Start the API:

    npm start
    
  6. 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), pino for structured logging, node-pg-migrate for schema migrations.
  • Database: PostgreSQL. Schema: an options table (one row per choice, tagged by category) and an option_connections table (weighted, undirected pairs between options) — see documentation/specs.md for the full data model.
  • Frontend: React 19 + Vite + Tailwind CSS 4 — see documentation/plan.md Phase 4.