With no SECRET_KEY set, Grimoire fell back to a fixed placeholder. The quickstart compose substituted change-me-in-production, so the README's own update step (pull, up -d) recreated the container on that placeholder, and every credential sealed under the key the user started with stopped opening. - config: with neither SECRET_KEY nor SECRET_KEY_FILE, the first start writes a random key to DATA_DIR/secret_key (mode 0600, published with a hard link so concurrent starts agree), and later starts read it back. An explicit key wins and leaves the file alone. A missing SECRET_KEY_FILE or an empty key file stops the start instead of switching keys. With no data directory (a run outside the container) the process gets a temporary key and says so. - app: warns on a public placeholder key, on a temporary key, and when /data is not a mounted volume. - Dockerfile creates /data. - docker-compose.yml is now the deploy compose (published image, both volumes, nothing to set). The build-from-source compose is docker-compose.dev.yml, and docker-compose.quickstart.yml is gone. The Swarm stack file gains the /data volume and keeps its explicit key, because a Swarm can move the app away from a node-local volume. - README: a Deploy section with the compose inline, SECRET_KEY optional, "keep both volumes", and updates that keep the key. A test holds the README's copy equal to the file. - Installs that ran on the change-me-in-production placeholder get a key of their own on the new compose; Settings then asks once for saved forge tokens and the backup secret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.3 KiB
Development
Running it
docker compose -f docker-compose.dev.yml up --build # app + PostgreSQL at http://localhost:5000
docker compose -f docker-compose.dev.yml up --build app # rebuild after a change
docker compose -f docker-compose.dev.yml down -v # stop and wipe the database
The image builds the frontend, so a frontend change needs a rebuild too. For quicker frontend work, run Vite against the running app; it proxies /api to port 5000:
cd frontend && npm ci && npm run dev # http://localhost:5173
Checks
CI runs these on every push; locally they need uv sync --extra dev and cd frontend && npm ci.
make lint # ruff check src/
make typecheck # vue-tsc --noEmit
make test # pytest tests/
make check # all three
Unit tests run without a database. Tests marked integration need a real PostgreSQL and run in CI's integration job, against a schema built by the real migrations.
Database migrations
The container runs alembic upgrade head on every start, after waiting for the database to accept connections.
docker compose -f docker-compose.dev.yml exec app alembic revision -m "short_description"
- Number revisions sequentially (
0123_...). Revision ids must fit in 32 characters, the width ofalembic_version.version_num. - Write both
upgrade()anddowngrade(). - Adding a value to a
CHECK-constrained column means dropping and re-adding the constraint in the same migration. - Keep migrations to schema changes. Filling a derived table from a large one belongs in a background backfill after startup, where its cost doesn't hold up the boot.
- A migration that depends on application code (a key derivation, a serialiser) copies what it needs rather than importing it, so a later edit can't change what an old migration does.
The plugin
plugin/ ships straight from git: installs fetch it through .claude-plugin/marketplace.json, and there is no build step. After changing anything under plugin/ or .claude-plugin/, run make mint-plugin before committing. Claude Code only refreshes an installed plugin when its version changes, and CI fails a plugin change that didn't bump it.
The hooks must run on a bare machine: POSIX shell, awk, sed and the usual coreutils, plus git and curl. They fail open, so a slow or unreachable Grimoire never blocks Claude Code.
CI and releases
CI runs on Gitea Actions (workflow in .forgejo/workflows/ci.yml) in the shared ci-python:3.14 image (ci-requirements.md lists what it relies on). Six jobs: TypeScript typecheck, plugin hooks, Python lint (plus design-token and dangling-style checks), unit tests, integration tests, and the image build, which waits on all of them except the plugin job.
| Push | Image tags |
|---|---|
dev |
:dev, :<sha> |
main |
:latest, :<sha> |
a v* tag |
:latest, :<version>, :<sha> |
:<sha> never moves, so it is the rollback point. Images publish to git.fabledsword.com/bvandeusen/fabledgrimoire, with a registry layer cache at :cache. The build needs the REGISTRY_USER and REGISTRY_TOKEN (a token with write:packages) repository secrets.
Work happens on dev. A release merges dev into main with a plain merge commit; a dated vYYYY.MM.DD.HHMM tag, matching the version the image reports, can mark it.
Conventions
Backend
- Services open their own session:
async with async_session() as session:(fromgrimoire.models). - Routes and MCP tools stay thin; the logic lives in
services/, shared by both. - Access checks go through
services/access.py, never inline ownership checks. - Every wait on something outside the process has a deadline.
Frontend
- API calls go through the typed helpers in
frontend/src/api/client.ts. - Pinia stores for shared state,
ref()for state local to a component; composables for reusable behaviour. - Colours, spacing and type come from the design system's tokens in
theme.css, which is generated, so CI rejects an unknownvar().
Commits
type(scope): what the change does
Why, and anything a reader tracing this change later needs.
Types: feat, fix, refactor, docs, chore, test. Comments in the code describe how it works today; the history of a change belongs in its commit message.