Changelog
Every released version of Kandan, newest first.
0.4.0
New
- The admin panel can see and change who pays. A user's edit form carries a billing exempt toggle, the list and detail views show the subscription status as a badge alongside the plan and renewal date, and the list query eager loads the subscription so the column costs no extra query per row.
- Billing gets its endpoints.
POST /api/v1/billing/checkoutreturns a hosted checkout URL for themonthlyoryearlyplan,GET /api/v1/billing/portalreturns a customer portal URL, andPOST /api/v1/hooks/billing/{provider}takes subscription events — verified against the Standard Webhooks signature before anything is written, logged like every other webhook, and answered with 202 whether it was applied, ignored or aimed at a customer we do not know. - Kandan now keeps subscription state of its own. A
subscriptionstable records the plan, status and current period end per user, an admin can mark a userbilling_exempt, and/api/v1/mereturnssubscribed,billing_exempt,plan,subscription_statusandcurrent_period_endalongside the existing fields. Payment processors sit behind a two-method seam — make a checkout URL, turn a webhook into a status change — with Polar as the one implementation. php artisan billing:setupcreates the two personal plan products in Polar and registers the webhook endpoint, then prints the product ids to paste into the environment. Prices are command options —--monthly,--yearlyand--currency, in the currency's minor unit — defaulting to EUR 10 a month and EUR 96 a year. It followsPOLAR_SANDBOXunless--sandboxforces it, and asks before it creates anything in a production organization.- Forwarded webhooks now send a normalized envelope —
id,source,title,body,actor,priority,url,created_at,forwarded_at,destinationand the untouched provider data underraw— signed asX-Kandan-Signature: sha256=<hex HMAC-SHA256>over the exact bytes sent, the same format incoming generic webhooks use. Breaking for existing webhook receivers: the old flat keys (notification_id,plugin_id,action_url,payload) and the old bare-hexSignatureheader are gone.PUT /api/v1/forward-destinations/{id}now merges the submittedconfiginto the stored one, so an update that omits the write-onlysecretkeeps it rather than dropping it. - The tray asks for a subscription. An account with neither a subscription nor an admin exemption gets a menu offering the web subscribe page and a sign out, instead of the inbox — and the tray now refreshes the signed-in user along with the counts, so a plan bought in the browser reaches the menu bar without signing out and back in.
- The front page says what Kandan costs: one personal plan at EUR 10 a month or EUR 96 a year, with the invite-only status note kept where it was.
- Manage forward destinations in the app: a Destinations page next to Rules and Groups lists every webhook, Slack and Discord destination with its failure count and auto-disabled state, and can create, edit, enable, disable, test and delete one — a failed test shows the delivery error inline. A rule's Forward action now picks its destination from a dropdown instead of asking for a raw ID, and the rules list names the destination it forwards to.
- Nothing in the app mentions teams while Kandan sells one personal plan. The Team Ownership blocks on Profile and Delete Account, and the Teams and Members panels on the debug dashboard, sit behind a single
teamsUiflag, and the delete-account warning no longer lists teams among what goes. The components, routes and API are untouched — flipping the flag brings all of it back. - The web app asks for a subscription. A new
/subscribepage offers the monthly and yearly personal plan and sends the customer to the processor's checkout, returning to/subscribe?done=1, which keeps asking for the session until the webhook lands rather than claiming the payment failed. Signed-in accounts with neither a subscription nor an admin exemption are redirected there from the app, and Account gains a Billing tab showing the plan, its status and renewal date, with a link into the processor's portal.
Fixes
User::hasAccess()is gone. The subscribe wall lives in the clients, which ask@repo/api-client's ownhasAccess(), and the API never called the method — it keeps ingesting regardless. Admin and/api/v1/mereadbilling_exemptand the subscription relation directly, as they already did.billing:setupno longer sendsorganization_id. Polar rejects the field when the request carries an organization access token, which is the only kind Kandan uses, so the product and webhook-endpoint calls now let the token name the organization andPOLAR_ORGANIZATION_IDis gone from the config and the env template.billing:setupsurvives Polar's webhook endpoint rules. A secret shorter than the 32 characters Polar demands — or none at all — is replaced with a generated one and printed as thePOLAR_WEBHOOK_SECRETline to set, anAPP_URLthat is not https skips registration with thepolar listencommand to use locally instead of failing, and the two product ids are printed as soon as the products exist, so a webhook step that goes wrong no longer costs them.- Only delivery frames on the notification stream carry an
id. State changes and connection events used to carry their own, so a client resuming after the five-minute reconnect could pick an older id as its resume point and get every newer delivery replayed as a fresh notification, which the desktop showed as duplicate banners. - Kandan gets its mark: a source dot and two arcs on the diagonal. The desktop app icon and menu-bar template use it, and the web app and site carry it as a favicon that follows light and dark mode.
- Desktop REST calls go through Rust. WebKit suspends networking for the hidden tray window, so the tray count and Recent list never refreshed after a notification arrived; the app now fetches via the Tauri HTTP plugin. The queries themselves also stalled: TanStack Query pauses retries for a window it thinks is backgrounded, which the tray window always is, and the first request went out before the stored token had loaded.
- The menu-bar glyph draws again and sits at the size of the system glyphs beside it. The count reads 0 instead of disappearing when the inbox is clear, and the stream starts before the tray is built so a tray failure can no longer take the banners down with it.
- The subscribe page stops asking eventually. After a checkout it polls for the payment confirmation every two seconds for a minute, then says so in one line and offers a Refresh that restarts the poll once, instead of leaving the tab refetching the session forever when a webhook never arrives.
- The authed layout asks
wallDecision()whether to wait for the session, send the visitor to/subscribe, or render the app, instead of spelling the rule out inline. Same behaviour, now a pure function the tests can hold to account.
0.3.1
Fixes
- Attach a notification's auto group to an existing group of the same slug instead of failing the webhook with a unique-constraint violation.
- The tray menu shows the app version, and the Tauri bundle version now follows
package.json, so it moves with every release instead of staying at 0.1.0.
0.3.0
New
- Native macOS banners for every notification the stream delivers, with the notification's action URL opening in the browser when the banner is clicked. Getting there also fixed four things that stopped the tray app working at all: a stale
plugins.storeconfig that panicked on start, an SSE URL missing its/apiprefix, a 401 on an anonymous request wiping the stored session, and no reconnect after the API's five-minute stream window closed.
Fixes
- Stream state changes that land in the same second as the SSE poll's checkpoint, so a "Mark all read" reaches open clients live instead of waiting for a reload.
- Widen
users.avatarto text: Google now returns avatar URLs longer than 255 characters, which made the first login of every new Google account fail with a 500 on the callback. - Read the notification stream in Rust instead of the webview. WebKit suspends the page whenever the tray window is off screen, so the stream stopped after its first frame and neither the tray counts nor the banners updated on their own. The frame handling in the webview is unchanged.
0.2.0
New
- Add a
/changelogpage, built at build time from the four apps' CHANGELOG files and grouped by version.