Bookward turns your reading history and the book lists you trust into a calm, personal shortlist. It runs on your own machine, shows why a recommendation appeared, and lets you send only the books you approve to Librarr.
It is for readers who want recommendations that feel personal without handing their entire library to another black-box service.
These screenshots use Bookward's bundled, sanitized demo catalog and default connection settings. A fresh install includes a small sample so you can explore before connecting your own library. The captures use dark mode with the Sahara theme. Desktop images are 1600 × 1000; mobile images use a 390 × 844 CSS pixel viewport and are saved at 2× resolution.
| Discover | Sources | Settings |
|---|---|---|
![]() |
![]() |
![]() |
Choose the feeds that shape your recommendations. A permanent source can refresh on a schedule; a one-time import stays in your discovery pool without being polled again. Settings is where you import reading history, choose an embedding provider, connect Librarr, and optionally configure a weekly digest.
- Your data stays with you. Bookward is self-hosted, uses SQLite, and does not ask for Goodreads credentials. The default Docker binding is localhost.
- Recommendations are explainable. Each book includes a match score, source, and plain-language reasons instead of an unexplained ranking.
- You control the inputs. Import your Goodreads history, keep or disable the bundled upcoming-book sample, and add public lists from publishers, booksellers, newsletters, or other trusted sources.
- Discovery stays focused. Shortlist recommendations, mark books Read to add them to your history, or find them in Librarr. Bookward does not track reading progress.
- Automation is optional and gentle. Scheduled source refreshes and weekly Discord or email digests are off until you choose them. Delivery is recorded and retryable.
- It is comfortable to use. The responsive interface includes keyboard-friendly controls, a skip-to-content link, accessible labels, light/dark/system modes, and reduced-motion support.
- Import a complete Goodreads CSV export, including ratings and read dates.
- Refresh recent Goodreads reads through a public read-shelf RSS feed.
- Combine reading history with author, subject, and text-similarity signals.
- Review recommendations in Discover, then shortlist, pass, or set one aside for later. Undo a recent choice, or revisit passed and deferred books from Past decisions. Mark a shortlisted book Read to add it to your history, with an optional rating.
- Search Librarr from a recommendation and add a matching ebook or audiobook directly.
- Choose the visual theme and appearance mode that work best for you.
- Start with a zero-download local hashing embedder that works on CPU-only machines.
- Switch to FastEmbed, Ollama, or an OpenAI-compatible remote embedding endpoint when you want to experiment.
- Add permanent feeds or one-time imports from public, HTTPS-accessible pages.
- Set source refresh cadence to manual-only, every 6 hours, daily, or weekly.
- Send weekly digests to Discord, SMTP email, or both, with minimum-score and “only new books” controls.
- SvelteKit frontend and FastAPI engine run independently, with no external scheduler required.
- SQLite stores a separate catalog, reading history, shortlist, settings, and jobs for each account in one persistent volume.
- Scheduled, WAL-safe database snapshots, manual backups, and an in-app restore flow help recover a self-hosted installation.
- Integration keys are encrypted in the engine database and are never returned to the browser.
- Public source fetching rejects private, loopback, link-local, and metadata addresses.
- Docker images are built in CI and published to GitHub Container Registry from version tags.
Bookward exposes a versioned API for automations and other applications. Open Settings → API access, name a token, and select Generate token. The full token is displayed only once; store it in the calling application and revoke it from the same screen if it is no longer needed.
When upgrading an existing install, restart the engine once so pending SQLite migrations are applied. Read history is preserved. The migration removes the old Up next, Reading, and Finished progress records.
The public base URL is:
https://your-bookward-host.example/api/v1
Send the token with every request:
curl https://your-bookward-host.example/api/v1/recommendations \
-H 'Authorization: Bearer bkw_…'The v1 API supports recommendations and feedback, source listing and management, Goodreads RSS imports, sync and scoring jobs, job status, and the Librarr search/download integration. The most commonly used routes are:
GET /api/v1/overview— recommendations, reading history, sources, and safe settings.GET /api/v1/recommendations— filter withstatus=recommended|saved|decisions|rejected|maybe_later|all, cap withlimit, and page withoffset.POST /api/v1/recommendations/{id}/feedback— send{"action":"save"},reject,maybe_later, orrestore. Onlyrejectis a negative learning signal;maybe_lateris neutral.POST /api/v1/recommendations/{id}/undo— send{"decision_id":123}from a recent feedback response to undo the latest choice.GET /api/v1/sources— list configured sources.POST /api/v1/sync— queue a source refresh and return a job ID.GET /api/v1/jobs/{id}— check a background job.
The engine also publishes the complete interactive OpenAPI schema at /openapi.json and Swagger UI at /docs when it is run directly. The web application proxy keeps /api/v1 available at the public Bookward URL.
Keep the engine port private; the versioned web proxy is the intended external API boundary. API tokens currently grant the full v1 API and do not expire, so revoke them promptly if they are compromised.
Bookward ranks books that it can find through your enabled sources; it does not invent titles or search every book in print. The ranking process is:
- Build a candidate pool. The bundled demo list, public feeds, and one-time imports provide books to consider. Goodreads CSV and RSS imports describe what you have read and how you rated it.
- Fill in available details. The engine checks catalog sources for descriptions, subjects, publication dates, and covers. It prefers a synopsis over an opening sentence, checks returned identifiers, and combines useful fields from verified records. Rated reads with valid ISBNs also receive a separate metadata cache. It removes books it recognizes as already read or already shortlisted from the recommendation feed.
- Compare book text. The engine turns each book's title, author, description, and available subjects into a numeric representation called an embedding. For the default local provider, similar representations mainly reflect shared words and two-word phrases. Optional model-based providers can also match related wording. Read documents retain their existing title/author, stored genres and description template; the separate enriched-read metadata cache is not injected into those documents. Candidate documents use their available description and up to eight normalized subjects. Representations are cached and refreshed when source text changes. Before reuse, the engine also checks that a matching cache entry decodes to its declared, finite float32 vector shape. Each scoring pass carries the validated read-vector dimension into its separate candidate-vector lookup, so cached or newly generated candidates with a different dimension are rejected before cache writes. Forced rebuilds apply the same cross-set check before replacing the active vectors.
- Rank likely matches. Strong matches to highly rated books raise a candidate; similarity to low-rated books lowers it. The engine also considers author ratings, read dates, and source weight. A wider rating kernel uses up to 40 of the most similar rated books, weighting cosine similarity to the eighth power; its adjustment is multiplied by
ESS / (ESS + 5), where effective sample size (ESS) measures how many reads meaningfully contribute. A separate exponential recency term has an eight-year timescale. Both terms ramp up with the size of the rated library and reach full strength at 100 distinct rated works. Sparse catalog details shrink the score toward a neutral 50. - Learn from observed choices. Saves, rejections, Librarr imports, and ratings attached to recommendations can adjust later rankings when weighted evidence is strong enough for the feature's 95% confidence interval to separate from the rest of the history. Ratings of 4–5 stars count as positive evidence; 1–2 stars count as negative. A later neutral action replaces an earlier choice for the same book. Maybe later, restore, three-star or unrated reads, and simply opening a book do not count as positive or negative evidence. These adjustments affect recommended books only, are reduced for sparse metadata, and are capped at eight score points overall.
- Reduce repetition in the first slate. After personalization and before pagination, a bounded pass considers the first eight recommended books. It may swap in a book from at most three positions later when the score cost is no more than five points, to avoid a third book by the same author or a near duplicate with cached-vector cosine similarity of at least 0.90. It changes order only; scores and explanations stay the same. See the discovery slate policy and its limits.
- Show the strongest reasons. Each card can point to a similar rated book, an author pattern, its source, or missing catalog details. These notes summarize useful evidence; they are not a line-by-line account of every scoring factor.
The displayed score is a 0–100 ranking signal, not a percentage chance that you will enjoy the book. It helps order the current candidate pool.
Verified work-level first publication year can contribute a confidence-gated era preference learned separately for each profile. The bounded adjustment is at most two points and activates only when every chronological held-out fold shows a rating-prediction gain with positive day- and author-cluster confidence bounds, and the candidate has at least eight effective local reads, authors, and days with a 95% correction interval that excludes zero. Missing, conflicting, edition-only, future, or unsupported years stay neutral. The latest captured production-history replay abstained and changed 0 of 370 candidate scores.
Scoring jobs now archive the exact read/candidate inputs, vectors, and outputs behind materialized base scores. Recommendation runs keep a separate bounded snapshot of serving inputs and actual cached vectors, plus copies of cards that were served and later attributed actions. These records survive routine catalog refreshes and can contain book text and reading interactions. See decision evidence and its retention controls for replay limits, incomplete-capture behavior, and privacy deletion requirements.
To evaluate ranking changes against a private history of rated reads, run engine/scripts/evaluate_historical_ratings.py on a consistent read-only snapshot. Its raw_ranking report measures whether high scores identify 4–5-star books and low scores identify 1–2-star books, with top/bottom groups and score-decile summaries. Keep these ordering diagnostics separate from the calibrated star-error report: calibration can change score ordering. See the historical replay protocol for the command and limitations.
All experiment findings, corrected measurements and adoption decisions are in the central experiment record. It includes the complete pipeline audit, enrichment and embedding comparisons, first-eight and combination studies, fresh reader judgments, and independent-reader checks. Richer read embeddings and the proposed 75/25 score blend were not adopted; the current kernel remains at power 8. Positive retrospective point estimates are not proof of live or cross-user recommendation gains.
Marking a recommendation or shortlisted book Read preserves its valid catalog ISBN even without a rating. Matching editions stay excluded when another source uses a different title or author spelling. Fresh, provider-consistent verified metadata work IDs also exclude matching editions, with identity proof retained in scoring evidence. The already-read audit and completed repair distinguish missing history from exclusion defects.
Association providers select bounded favorite seeds from the complete reading library and exclude known reads outside that seed set, including disliked and unrated books. Their shared checks use title/author, provider-scoped work IDs and valid ISBNs. Known-author title collisions alone do not suppress a different work; an unknown author on either side retains a conservative title fallback. See the association identity findings.
Metadata backfills run in bounded background batches, including on existing installations. Candidate changes queue rescoring; empty or unverified read lookups are cached for 30 days; temporary provider failures become eligible for retry after 24 hours. These cooldowns also apply to failed lookups for a known work ID; an identity change makes the read eligible immediately. Each stored field records its provider and a bounded source projection. Full-title matching ignores known format and numbered-series annotations while preserving substantive subtitles, and publication dates retain their year, month, or day precision. Open Library requests share a one-request-per-second budget, and an unsuccessful lookup preserves an existing real cover.
Open Library and Google Books can supply different parts of a record. The Google Books discovery API key is also used for metadata lookups when configured. A provider outage or rate limit is recorded separately from a catalog identity mismatch. Verified read metadata is cached separately for inspection and future evaluations; it is not added to the historical scoring embeddings. Enrichment preserves imported titles, authors, ISBNs, ratings, and read dates; changing a read's identity invalidates its cached metadata.
- The source list sets the boundaries. A book outside your enabled feeds and imports is not available to rank. Incomplete or quiet sources can make the pool small or repetitive.
- Book details can be sparse or wrong. Public catalogs do not always have reliable summaries, subjects, dates, or edition matches. Sparse details reduce the influence of text similarity, but the engine cannot fill in information that is missing.
- Reading history only tells part of the story. Ratings and titles do not explain why you liked a book, what mood you were in, or what you want to read next. The default local text matcher is simple; richer embedding models still depend on accurate book text.
- Personalization needs feedback. A small number of ratings or shortlist/pass actions may not reveal a stable preference. An unseen or unrated book is not treated as a negative signal.
- The score is a heuristic. Scores are designed to rank the available books, not to promise enjoyment or make an objective quality judgment. Explanations can also be brief when there is little evidence.
The default hashing provider runs on the Bookward engine host. If you choose a remote embedding endpoint, the book text used to create embeddings is sent to that endpoint for processing.
Goodreads CSV / RSS + trusted public lists
|
v
FastAPI engine + SQLite
import -> enrich -> score
|
v
SvelteKit UI
explain -> shortlist -> decide
|
+---------+----------+
| |
Librarr Discord / SMTP
(optional) weekly digest
Bookward is deliberately split into a friendly web UI and an independent engine. The engine keeps working when the browser is closed: it owns source refreshes, scoring jobs, cover enrichment, digest scheduling, and the SQLite shortlist and read history. See the reading workflow guide for the shortlist and read-history behavior.
The first eight discovery recommendations use a bounded anti-redundancy pass to reduce third-author repeats and near-duplicate books; see the discovery slate policy for its limits and telemetry.
- Node.js 24 or newer
- Python 3.12 or newer
- uv for the Python environment
- Docker and Docker Compose are optional for local development
git clone https://github.com/sgerner/bookward.git
cd bookward
npm ci
uv sync --project engine --extra dev --frozen
If you do not use uv, create a virtual environment and install the engine package directly:
python3.12 -m venv engine/.venv
. engine/.venv/bin/activate
pip install -e engine
From the repository root, in one terminal:
AFTERWORD_DB=./data/afterword.db \
uv run --project engine uvicorn afterword_engine.main:app \
--reload --host 127.0.0.1 --port 8000
The AFTERWORD_ environment prefix and afterword_engine Python package are retained for compatibility with the project's earlier name.
In a second terminal:
ENGINE_URL=http://127.0.0.1:8000 \
npm run dev -- --host 127.0.0.1 --port 5173
Open http://127.0.0.1:5173. The engine creates the SQLite database and a clearly labeled, sanitized demo catalog on a fresh install. Those books are sample data, not a live editorial feed; disable the demo source from Sources when you are ready to use your own inputs.
On a new install, open /login and choose a username and password for the first administrator. The first account created claims administration, and setup closes automatically afterward. Existing installs with AFTERWORD_AUTH_USERNAME and AFTERWORD_AUTH_PASSWORD migrate those credentials to the first administrator account automatically. After that migration, account passwords live as Argon2id hashes in Bookward's identity registry; the environment password is no longer used.
- Open Settings and import a full Goodreads CSV export. RSS is useful for incremental refreshes, but a CSV export is the way to bring in your complete history.
- Open Sources and keep only the public lists you trust. Add your own permanent feeds or one-time imports when you find a good list.
- Review the explanation on each recommendation, then shortlist the books you want to keep.
- Optionally connect Librarr under Settings and choose ebook or audiobook as the default waitlist format.
- If you want a nudge, configure a weekly Discord or email digest. Use the per-channel test buttons before enabling it.
Docker is the easiest way to run Bookward as a small self-hosted service.
cp .env.example .env
Set ORIGIN and AFTERWORD_PUBLIC_URL to the exact URL readers will open. Compose embeds ORIGIN in the web build, so rebuild with docker compose up --build after changing it. Generate a private service secret with openssl rand -hex 32 and set ENGINE_SERVICE_SECRET; this protects private engine routes from other containers on the Docker network. The default BIND_ADDRESS=127.0.0.1 keeps the service local. If AFTERWORD_AUTH_PASSWORD is already configured on an existing install, its username and password become the first administrator account on upgrade. For a new install, the first person to complete setup at /login becomes the administrator.
docker compose up --build -d
Open http://127.0.0.1:3000. Bookward stores the identity registry, each profile's SQLite database, and the generated encryption key in the afterword-data volume. Stop the stack with:
docker compose down
Bookward creates an online backup for each active profile every 24 hours and keeps the latest 7 profile snapshots in the separate afterword-backups Docker volume. It also backs up the account registry under auth-registry/. Each profile snapshot has a matching generated-key recovery file beside it; both files are owner-only in private directories. Settings shows the current profile's last successful backup, lets that profile create a snapshot, and restores only that profile after you type RESTORE. Restore makes an extra safety snapshot first. Configure BACKUP_INTERVAL_HOURS=0 to disable scheduled snapshots; BACKUP_RETENTION_COUNT changes how many scheduled or manual snapshots are kept. Set BACKUP_DIR only to a directory mounted persistently into the engine container. For disaster recovery, preserve the afterword-backups volume separately from afterword-data; docker compose down -v removes both.
Profile snapshots include that profile's data and encrypted integration credentials. The shared identity registry has a separate operator recovery command. Stop the engine before restoring it: docker compose run --rm engine python -m afterword_engine.auth_admin list, then docker compose run --rm engine python -m afterword_engine.auth_admin restore <snapshot-id> --confirm. Restore revokes all restored sessions and API tokens; users sign in again and integrations need new tokens. When Bookward generated the Fernet key, the private backup volume keeps a matching secret.key recovery file for each profile snapshot; restoring a snapshot restores its generated key too. If you set AFTERWORD_SECRET_KEY yourself, that value is not copied into backups and must be restored from your deployment's secret store. Snapshot and key files never appear in browser responses. Keep both Docker volumes or copy the private backup directory to secure storage outside the host.
The first administrator can create accounts at Manage accounts. Each account owns one profile, which Bookward opens automatically after sign-in; there is no profile-selection step. A password is a way to authenticate an account, not a profile identifier. New accounts receive a temporary password set by the administrator and must change it on first sign-in. Readers can link an OpenID Connect identity under Account after signing in; later SSO sign-ins resolve that identity to the same account and profile. Matching email addresses never link accounts automatically.
Each profile has its own data store. Candidate pools and discovery decisions, read history, shortlists, sources, recommendations and learning history, jobs, API tokens, and integration settings stay with that profile. Signing in to another account opens its separate data. Embeddings are also stored per profile today, so the same book may be embedded more than once.
Configure one OpenID Connect provider with OIDC_ISSUER, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET. OIDC_REDIRECT_URI is optional; when omitted, Bookward uses {AFTERWORD_PUBLIC_URL}/auth/oidc/callback. Set OIDC_AUTO_PROVISION=true only when every authenticated identity from the configured issuer should receive a profile automatically. Automatically provisioned identities receive regular reader accounts with new profiles; OIDC does not automatically create administrators. The default is false, so SSO identities must first be linked to an existing account. SSO uses authorization code flow with PKCE and validates issuer, audience, signature, state, nonce, and redirect configuration.
Set OIDC_AUTO_LOGIN=true to send unauthenticated visitors directly to the configured SSO provider instead of showing the Bookward login form first. After sign-out, Bookward shows the login form without immediately signing the same browser back in. Use /login?manual=1 to open the local login form directly; OIDC_AUTO_LOGIN defaults to false.
Candidate pools, reads, shortlists, sources, learning history, jobs, API tokens, and integration settings are stored in separate SQLite files per profile. Bookward currently keeps embeddings inside each profile database; this spends more disk and compute while preserving the same isolation boundary.
The default local embedding backend needs no model download and works on CPU-only machines. Optional alternatives include FastEmbed, an Ollama model, or an OpenAI-compatible endpoint. OpenAI-compatible endpoints must return one vector per input with unique integer indexes from 0 to N−1; responses without indexes are rejected because their input order cannot be verified. Use the Ollama backend for Ollama's separate embedding protocol. To try Ollama locally:
docker compose --profile ollama up --build -d
docker compose exec ollama ollama pull qwen3-embedding:0.6b
For NVIDIA acceleration, install the NVIDIA Container Toolkit and add the GPU Compose override:
docker compose -f compose.yaml -f compose.gpu.yaml \
--profile ollama up --build -d
If Librarr runs in another container, put both services on the same Docker network, allow its hostname through LIBRARR_ALLOWED_HOSTS, and use its internal URL (usually http://librarr:5050) in Settings.
Copy .env.example to .env for Docker, or export variables in the shell for a local run.
| Variable | Purpose |
|---|---|
ORIGIN |
Canonical browser origin embedded in the web build. |
AFTERWORD_PUBLIC_URL |
Base URL included in Discord and email digest links. |
BIND_ADDRESS |
Host interface for the Docker web port; keep 127.0.0.1 unless you have a secured deployment. |
AFTERWORD_AUTH_USERNAME / AFTERWORD_AUTH_PASSWORD |
Optional one-time bootstrap credentials for the first administrator, or compatibility migration from existing Basic authentication. |
ENGINE_SERVICE_SECRET |
Private credential shared by the web and engine containers. |
OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET |
Optional OpenID Connect provider configuration. |
OIDC_REDIRECT_URI |
Optional exact callback URL registered with the provider. |
OIDC_AUTO_PROVISION |
Allow any valid identity from the configured issuer to create a profile; defaults to false. |
OIDC_AUTO_LOGIN |
Redirect unauthenticated visitors directly to the configured OIDC provider; defaults to false. |
AFTERWORD_DB |
SQLite path for a local engine run. Docker uses /data/afterword.db. |
EMBEDDING_BACKEND / EMBEDDING_MODEL |
Provider and model selected by the engine. |
EMBEDDING_URL / EMBEDDING_API_KEY |
Endpoint and optional key for remote or Ollama providers. |
SOURCE_SYNC_INTERVAL_HOURS |
First-install default for permanent-source polling; the UI can change it later. |
AFTERWORD_SECRET_KEY |
Optional Fernet key; if omitted, Docker generates one in its data volume. |
BACKUP_INTERVAL_HOURS |
Scheduled SQLite snapshot cadence; set 0 to disable. Defaults to 24 hours. |
BACKUP_RETENTION_COUNT |
Number of newest automatic and manual snapshots to retain. Defaults to 7. |
BACKUP_DIR |
Optional backup path inside the engine container; mount it persistently when set. Defaults to /backups, on its own afterword-backups Docker volume. |
LIBRARR_ALLOWED_HOSTS |
Comma-separated private hostnames allowed to receive the Librarr API key. |
The engine also persists source cadence and application settings in SQLite, so changes made in the UI survive restarts.
Run the same checks used by CI before opening a pull request:
# Frontend
npm run check
npm test
npm run build
# Engine
uv sync --project engine --extra dev --frozen
uv run --project engine pytest -q engine/tests
uv run --project engine python -m compileall -q engine/afterword_engine
# First-run web/engine integration, with isolated temporary data (requires the web build)
uv run --project engine python scripts/community_smoke.py
# Container smoke build
docker compose build
The frontend suite covers Svelte/type checks and unit tests. The engine suite covers imports, source validation, scoring, jobs, Librarr idempotency, digest delivery, and secret handling. The GitHub Actions workflow also runs CodeQL, dependency review, and both Docker image builds on pull requests.
Bookward is easier to improve when changes are small, understandable, and easy to try. A good contribution can be a bug fix, a clearer explanation, an accessible UI improvement, a new source adapter, or a better developer workflow.
-
Open an issue for a larger behavior change, or start with a focused branch from
main:git switch -c feat/describe-your-change -
Make the smallest change that solves the problem. Keep product copy friendly and direct.
-
Run the relevant checks from Testing and quality checks.
-
For UI changes, include before/after screenshots in the pull request and check the layout at narrow and wide widths.
-
Keep controls keyboard reachable, provide labels for new inputs, preserve visible focus, and respect
prefers-reduced-motion. -
Open a pull request against
mainwith a short summary, testing notes, and any setup or migration detail a reviewer needs.
Please do not commit .env files, database files, API keys, webhook URLs, SMTP credentials, or generated secret keys. Use the sanitized demo catalog or redacted fixtures when adding examples. See SECURITY.md for vulnerability reports.
src/ SvelteKit UI, form actions, and browser theme handling
engine/ FastAPI ingestion, jobs, scoring, SQLite, and integrations
engine/tests/ Python engine tests
src/**/*.spec.ts Frontend and server unit tests
static/ Favicon, manifest, and static web assets
docs/screenshots/ README screenshots from the sanitized demo
compose.yaml Local Docker stack
compose.gpu.yaml Optional NVIDIA/Ollama override
.github/workflows/ CI, CodeQL, dependency review, and image publishing
The UI intentionally shows a retryable 503 page when the engine is unavailable. Start the engine first and confirm that ENGINE_URL points to the same host and port as uvicorn.
The default local embedder is intentionally small and predictable. FastEmbed, Ollama, and remote providers may download a model or take longer during their first indexing pass. Check Settings and the engine terminal for progress.
Use the full Goodreads CSV export for historical data. The RSS feed is designed for incremental refreshes of recent reads, not for reconstructing an entire library.
Goodreads CSV and manual historical imports contribute time-based read outcomes only when they include a valid completion date. Their import time is not treated as the date the book was read; current manual read marks still use their creation time when no date is supplied.
The URL must be reachable from the engine process, not just from your browser. Use http://librarr:5050 between containers and http://127.0.0.1:5050 when both services run on the host.
Every push to main and every pull request runs frontend checks, engine tests, Python compilation, CodeQL analysis, dependency review, and both Docker builds. Dependabot checks npm, Python, and GitHub Actions dependencies weekly.
Pushing a tag matching vMAJOR.MINOR.PATCH runs the complete CI checks before publishing the Bookward and engine images to GitHub Container Registry as the version tag and latest. Manual publishing must also select a version tag; branches cannot overwrite release images.





