English · Português
All notable changes to this project are documented here. The format follows Keep a Changelog.
Versioning is SemVer with 0.x semantics, the Cargo convention:
0.MINOR.0is a breaking release. It can change the HTTP API, rename or remove aQUARK_*variable, or change the on-disk format.0.MINOR.PATCHis compatible features and bug fixes.0.MINOR.0-rc.Nis a pre-release and never gets thelatestimage tag.
The public contract covered by those numbers is: the HTTP API (/, /:code,
/:code/stats, /admin/*), the QUARK_* variables, the LMDB on-disk format
and the Postgres migrations, and the webhook payload and signature. The Rust
library surface in src/lib.rs, the admin panel HTML, and the ClickHouse table
layout are not covered.
0.5.0 - 2026-08-09
- Open core. The repository is now AGPL-3.0-only everywhere except
src/ee/andweb/src/ee/, which are covered by the quark Enterprise Edition License (src/ee/LICENSE). The Enterprise code sits behind the non-defaulteecargo feature and a single router injection point; deleting both directories leaves a buildable, passing core, and CI proves that on every push. What lives there is what only makes sense operating quark as a service for other people: workspace administration, invites, per-tenant IdP, Keycloak provisioning, custom-domain verification. Nothing on the redirect path is gated, ever. Seedocs/LICENSING.md. - Plans and entitlement (cloud). A tenant now carries a billing plan
(Free, Starter, Pro, Business, Custom); the catalog of limits lives in code,
versioned with the features it limits. Gated endpoints answer
402 Payment Requirednaming the limit hit, the ceiling and the cheapest plan that lifts it: webhooks, Sheets and pixels by feature, custom domains and members by ceiling, per-tenant SSO by plan.GET /admin/planreports the tenant's plan, limits and unlocked features so the panel never carries its own copy of the grid, andPUT /admin/tenants/{id}/planis the operator escape hatch. The Community edition never enforces any of this: a self-hosted AGPL install stays free and unlimited. Seedocs/PLANS.md. - Stripe billing (cloud).
POST /admin/billing/checkoutopens a hosted Checkout Session: Owner only, currency (usdorbrl) chosen on the first checkout and locked to the customer from then on, and a 14-day no-card trial granted once per workspace.POST /admin/billing/portalopens the Customer Portal, where upgrade, downgrade, cancellation, cards and invoices live.POST /stripe/webhookis the only automatic writer of the plan: signature-verified, deduplicated by event id, and it always re-fetches the subscription from the API instead of trusting the event payload, so out-of-order deliveries cannot downgrade a paying workspace.past_duekeeps the plan through the retry window; terminal states drop to Free; downgrades never delete anything, they only block new creation. Enabled byQUARK_STRIPE_SECRET_KEY,QUARK_STRIPE_WEBHOOK_SECRETandQUARK_STRIPE_PANEL_URL, all three or nothing; without them the billing endpoints do not exist. Seedocs/BILLING.mdanddocs/RUNBOOK-stripe.md.
- CI runs on Node 22 (Node 20 is end-of-life), and a dependency wave landed:
the tokio stack,
base640.23,criterion0.8, and the React, Vite and npm groups on the panel.
0.4.1 - 2026-07-27
- Workspace deletion.
DELETE /admin/tenants/{id}removes a workspace and everything scoped to it, in one transaction, plus its ClickHouse click events and its Keycloak realm. Only theOwnercan delete, the last remaining workspace cannot be deleted, and a tenant the caller is not a member of answers404rather than403so the endpoint cannot be used to enumerate workspaces. The panel asks for the slug to be typed before it will proceed. Deleting frees the slug for reuse. Seedocs/WORKSPACES.mdfor what is removed immediately and what is removed eventually. - Workspace creation explains the wait. The request provisions a realm, a client, a mapper and a user in Keycloak, so it is legitimately slow. The panel now says a sign-in is being prepared, and after a threshold adds that it is taking longer than usual and that reloading is safe.
disabled_reasonon the webhook representation inGET /admin/webhooks, which lets the panel tell a subscription the user paused from one the system disabled.
- Webhook destination URLs no longer reach the logs. For Discord, Slack,
Telegram and the generic connectors the token lives in the URL path, so the
URL is the credential. It was printed in full at ten log sites. The field is
now a
WebhookUrlwhoseDisplayprints host and port only, andreqwestdoes not accept it without an explicitexpose(), so reintroducing the leak is a compile error rather than a review miss. The subscription's signing secret was also readable through the struct'sDebug, and no longer is. - A dead webhook destination is no longer retried forever.
404and410mean the destination is gone, but every non-2xx response was treated the same, so a removed endpoint burned the full attempt budget on every event, indefinitely. Those two statuses now get one confirmation attempt, and a destination that fails it is disabled with the reason recorded and shown in the panel.429,5xx, timeouts and transport errors keep the existing backoff.400and422are deliberately not treated as permanent: they usually mean our payload is wrong, and disabling a customer's integration over our own bug is the worst outcome available. - Reconnecting Slack over a disabled subscription reactivates it. The OAuth
merge inherited
active: falseand the old reason, so the obvious fix (reconnect) appeared to work and delivered nothing. - Log events from
main.rscarry fields again. Eight sites built JSON by hand inside the log macro, which underQUARK_LOG_FORMAT=jsonproduced a serialized object escaped insidemessage, so the values were text rather than queryable fields. Six of them logged errors atinfo, which no alert keyed on severity would ever match.
0.4.0 - 2026-07-26
- BREAKING:
QUARK_ACCESS_LOGis gone. The per-request access log now comes from tower-http'sTraceLayerand is emitted atDEBUG, so it is off under the defaultinfofilter and turned on withRUST_LOG=tower_http=debug. A deployment that still setsQUARK_ACCESS_LOGwill not fail, but it will no longer produce an access log. The newQUARK_LOG_FORMAT=jsonswitches every log event to one JSON object per line, which is what a log pipeline wants. - Errors are typed with
thiserroracross the crate, and logging goes throughtracinginstead ofeprintln!. Nothing about the HTTP contract changes: handlers return the same statuses and the same short error bodies. - Signing keys are held in
secrecy::SecretBoxso they are zeroized on drop and cannot be printed by accident. - Dependencies: axum 0.7 to 0.8, heed 0.20 to 0.22, redis 0.27 to 1.x, sqlx 0.8 to 0.9. No on-disk format, migration or wire format changes with them.
- SSRF:
[::127.0.0.1]was accepted as a destination. The internal-address check existed in two copies that had drifted apart, and the link-creation one did not reject IPv4-compatible IPv6 addresses. There is now a singleis_internal_ipcovering IPv4-mapped and IPv4-compatible IPv6, CGNAT (100.64/10),0.0.0.0/8, multicast and the documentation ranges. - The OIDC login
stateis compared in constant time. POST /admin/logoutuses the shared CSRF guard instead of its own header check, so it accepts the same proofs every other state-changing endpoint does.- A dropped click event (analytics channel full) is counted and logged instead of vanishing, so saturation is visible. The counter only runs on the drop path, so a healthy redirect pays nothing for it.
- OIDC configuration reads each required variable once and carries the value, rather than validating the variable and reading it again.
unsafe_code = "deny"and a clippy policy (unwrap_used,expect_used,panic,await_holding_lock,let_underscore_future) are enforced in CI. The 28 remainingexpect()insrc/each carry a written justification.
0.3.1 - 2026-07-25
- OIDC login no longer crashes the server: the
jsonwebtoken10 upgrade shipped without a crypto backend, so validating any id_token panicked and restarted the process. Therust_cryptofeature is now pinned and a canary test exercises a real JWT operation in CI so this class of regression fails the build instead of production.
0.3.0 - 2026-07-25
- Fully responsive admin panel (mobile, tablet, desktop): navigation drawer with hamburger on small screens, full-screen create/edit link dialogs on phones, per-screen reflow down to 360px wide, and 44px touch targets on primary controls.
- Local responsive QA script (
web/scripts/responsive-qa.mjs): sweeps every screen across 4 breakpoints and both themes, failing on any horizontal overflow.
- Production deploys are now release-driven: only version tags trigger a deploy, through a single release workflow.
- Major dependency upgrades: axum 0.8, ClickHouse client 0.15, chacha20poly1305 0.11, redis 1.4, plus React/Vite toolchain bumps.
- Stats charts no longer break when a tooltip label is not a string.
- OIDC id_token validation now requires the
exp,issandaudclaims.
0.2.0 - 2026-07-24
First tagged release and first published container image. Everything below has
been in main since the project started; this entry marks the point where it
became installable.
- Short codes computed by a calibrated Feistel network with an ARX round function, a bijection over the id space with no code index kept on disk.
- Pluggable storage: embedded LMDB (default, zero-dependency) or Postgres for a multi-node, shared-database deployment.
- Pluggable cache: in-process by default, with an optional Valkey L2 tier and cross-node invalidation over Valkey pub/sub.
- Pluggable analytics: an embedded sink by default, or ClickHouse for an OLAP
analytics backend;
GET /:code/statsfor aggregates and recent events. - OIDC login (Authorization Code + PKCE) as an alternative to the admin token, with opaque revocable server-side sessions.
- Signed outgoing webhooks following the Standard Webhooks spec, on
link.created/updated/deleted/expired/clicked/broken/recovered; a durable Postgres outbox with retry, backoff and dead-lettering, best-effort delivery on LMDB; Slack/Discord/Telegram notification channels built on the same subscription model. - API tokens with scopes (
links_read,links_write,webhooks,analytics,full) and an optional per-token rate limit. - Redirect rules: per-link geo/device targeting, first match wins.
- A/B testing: weighted link variants with per-variant click stats.
- Deep linking: hosts the iOS
apple-app-site-associationand Androidassetlinks.jsonfiles, plus device-aware redirect to an app destination. - Password-protected links (argon2id), max-visits expiration with an optional fallback URL, and broken-link monitoring with webhook notifications on status transitions.
- Conversion forwarding to GA4 and Meta CAPI, dispatched off the redirect hot path.
- Importer for CSV/JSON exports from Bitly, Kutt, YOURLS and a generic format, with a partial-success per-row report.
- Tags, a UTM builder with locally saved templates, and server-side search on Postgres (client-side fallback on LMDB).
- Abuse protection on link creation: per-IP rate limiting and a built-in guard against internal/loopback network targets (SSRF).
- Admin panel (React, Vite, shadcn/ui, TanStack, Recharts): link CRUD, search, tags, QR codes, per-link stats, API token management.
docker-compose.ymlfor a full local stack (quark, Postgres, Valkey, ClickHouse).quark --versionand anX-Quark-Versionheader onGET /health.
- AGPL-3.0-only core with a CLA collected on every pull request.
- Private vulnerability reporting and a written security policy.