Repository navigation
v2.0.0
Upgrade guide: README › Upgrading to v2.
Staying on v1? Pin image tags to 1.1.0 (the last v1 release).
⚠ BREAKING
-
One image, one port; the API moved under
/api. Web UI and API ship as one image,
qorstack/report(rootDockerfile), on port 8080: web UI at/, API at/api
(e.g.POST /api/render/word/template, downloads at/api/files/{token}, health at/api/health,
reference at/api/docs).qorstack/report-api/qorstack/report-weband port 3000 are gone.
Compose has oneappservice;API_URL,FRONTEND_PORT,REPORT_API_TAG/REPORT_WEB_TAGand
NEXT_PUBLIC_SERVICEwere removed — setPUBLIC_URL(andAPP_PORT).CORS_ORIGINSis now only
for other origins. API clients add/apito the base URL; SDK 1.0 and the MCP server do it
automatically. GitHub sign-in config moved to the backend (GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,
GOOGLE_CLIENT_ID), and the code exchange now runs server-side. -
Storage uses one bucket on any S3 server.
STORAGE_BUCKET(defaultqorstack-report)
with folderstemplate/,generated-report/(incl.temp-download/, deleted after 1 day)
andfonts/replaces thetemplates/reports/fontsbuckets.MINIO_BUCKET_TEMPLATE,
MINIO_BUCKET_REPORT,Minio:TemplateBucketandMinio:ReportBucketwere removed.
Copy existing data once withselfhost/migrate-storage.sh keep-minioorto-rustfs(rclone);
the backend logs a warning when old-bucket data was not migrated. -
RustFS is the default storage. The self-host compose starts RustFS (Apache-2.0) with no
extra settings; MinIO stays available through thedocker-compose.minio.ymloverride (image pinned
from quay.io — MinIO left Docker Hub), anddocker-compose.external-s3.ymlpointsSTORAGE_*at
any external S3. Unreachable storage now gives a clear 503 and an unhealthystoragehealth check. Settings areSTORAGE_*/Storage:*; v1MINIO_*/Minio:*still work as fallbacks. -
Downloads go through the API. Every link (
downloadUrl, previews, fonts, history) is now
{PUBLIC_API_URL}/files/{token}— signed, time-limited, streamed by the backend with Range
support — instead of a presigned storage URL. Storage no longer needs a public address:
MINIO_PUBLIC_ENDPOINTis no longer needed and port 9000 is not published. -
Postgres is no longer published on the host (port 5432), so it can't clash with a local
Postgres. Tools that connected from the host (DBeaver, psql) need the commented
127.0.0.1:${DB_HOST_PORT:-5432}:5432line underpostgresindocker-compose.yml, ordocker exec. -
font-syncer uses rclone (
rclone/rclone) instead of the MinIO client, so it works with
any S3 server. -
Render request shape. Every v1 alternative form was removed and now returns
400
naming the field and the expected shape:replace: { "customer": "ACME" } → [{ "key": "customer", "value": "ACME" }] image/qrcode/barcode: { "logo": { "src": "…" } } → [{ "key": "logo", "src": "…" }] table rows: [{ "item": "A", "qty": 2 }] → "columns": ["item", "qty"], "rows": [["A", 2]] generateTotals / numberFormat: { "amount": "sum" } → [{ "key": "amount", "value": "SUM" }] sort: "date desc" → [{ "field": "date", "direction": "desc" }] verticalMerge / collapse: "group" → ["group"]Row length must match
columns; sort / merge / totals must name existing columns. -
POST /render/excel/templateis Excel in, Excel out —fileTypeand the PDF
options were removed (always.xlsx). -
Image URLs in requests may no longer point at private or internal addresses
(localhost, 10.x, 172.16–31.x, 192.168.x, 100.64–127.x, cloud metadata). Set
IMAGE_FETCH_ALLOW_PRIVATE_NETWORKS=truefor intranet image servers.minio:paths are
accepted only for the template's own builder assets. -
zipOutputis a Pro feature (it was documented as Pro but never enforced); without Pro it
now returns403. -
Website is dark only (the light theme and theme toggle were removed).
-
SDK 1.0.0 (Node / .NET) uses the new types. Stay on API 1.1.0 / SDK 0.x if you still
send the old forms.
Added
- Project export & import (Pro): Project settings → Export & import. Export downloads one ZIP
(manifest + latest active version of every template, its test data and builder images, uploaded
fonts); import shows a preview (create / update / skip) and then creates new templates with the same
key and adds newer versions — equal or older versions are skipped.GET /projects/{id}/export,
POST /projects/{id}/import?dryRun=true|false; size limitPROJECT_IMPORT_MAX_MB(200). - Request limits (env, 0 = unlimited):
RENDER_MAX_BODY_MB(30),RENDER_MAX_TABLE_ROWS
(50,000 per table),RENDER_MAX_ITEMS_PER_FIELD(1,000 per replace / image / qrcode / barcode).
Over a limit →400naming the field and the setting. - Validation: all errors in one
400, keyed by JSON path (table[0].rows[12],replace[3].key). templateVersion(optional) renders a stored older version; unknown version →404.- Async renders (Pro):
"async": truereturns202 { jobId, status: "pending", statusUrl };
pollGET /render/jobs/{id}(freshdownloadUrlon success). OptionalwebhookUrlreceives
POST { jobId, status, downloadUrl?, expiresIn?, error? }, signed with
X-Qorstack-Signature: sha256=<HMAC>whenWEBHOOK_SIGNING_SECRETis set; delivery state is stored
on the job (migration006_webhook-delivery.sql, 3 tries: now, +10 s, +60 s), so retries survive restarts.
The queue lives in PostgreSQL (report_jobs, migration005_render-queue.sql): jobs survive restarts,
several backend instances can share it (FOR UPDATE SKIP LOCKED,LISTEN/NOTIFY), a crashed worker's job
is picked up again when its lease ends, and infrastructure errors are retried (10 s, 60 s; 3 tries).
Settings:RENDER_ASYNC_WORKERS(2),RENDER_JOB_LEASE_SECONDS(300),RENDER_POLL_SECONDS(5),
RENDER_MAX_PENDING_JOBS_PER_USER(100 →429). - Website: new Press design, new logo/icons and a generated link-preview image.
selfhost/migrate-storage.sh, fully commented.env.examplefiles,init.sh/ migration script
downloadable from/selfhost/.
Fixed
- Public Excel API calls no longer overwrite the template's saved builder payload.
- Server-side request forgery through image URLs and cross-template
minio:paths. System.Security.Cryptography.Pkcspinned to 10.0.13 (CVE-2026-84008, CVE-2026-70162; it comes in
through Microsoft.Data.SqlClient). Image builds failed restore on these advisories.- Startup: database migrations finish before anything else starts (render worker, storage, HTTP),
so a fresh upgrade no longer logscolumn ... does not exist; a failed migration stops startup. - Project pages follow the project in the URL: a stale or deleted project link shows
"Project not found" instead of another project (Settings › Delete acted on the wrong one). - Account: a failed password change no longer reports success; a failed GitHub sign-in shows an
error instead of spinning; the system admin is told their login comes fromADMIN_EMAIL/
ADMIN_PASSWORD; Google/GitHub buttons show only when configured. - Editor: test values save automatically (with a leave-page warning while unsaved); Rename no
longer sends inline images in the URL; "Make current" asks first; Generate opens the preview on
mobile; Excel tables get the same summary and filter as PDF. - Excel editor no longer runs a hidden render each time it opens (it added a History entry and
its result was never used). - History and Templates filter and search on the server, so rows beyond the first page are found;
live polling no longer duplicates rows. - Template gallery shows each template rendered with its sample data
(GET /templates/gallery/{id}/thumbnail; regenerate withpython gallery/_thumbnails.py). The gallery
samples use the v2 request shape, so they can be sent to the API as-is, and the sales report's total comes fromgenerateTotalsinstead of an unsupported{{table_sum:...}}marker. - Self-host:
verify.shreadsAPP_PORT; docs and the agent prompt use theappservice,bash verify.shand
docker compose up -d; one Free vs Pro list across the site; SDK examples include/api.
Docker: docker pull qorstack/report:2.0.0 · .NET SDK: dotnet add package Qorstack.Report.Sdk · Node SDK: npm i qorstack-report-sdk