Drop the constraint that limited a member to one billing account
Finds the constraint by shape rather than by name — Hibernate generated it and the name is not stable across environments, so any unique constraint on account_m...
Let members and groups pay for themselves, under the owner's rules
Three things were missing and one was actively harmful. account_members carried a global unique on member_id — a member could administer exactly ONE billing ac...
Expose the fail-open impact to the operator
The app catalog page already answers the org-independent question — does any plan mention this app — and raises a warning when none does. This answers the tenan...
Show what turning off fail-open would cost
kamo.entitlement.fail-open grants any app the plan matrix never mentions. It is documented as a temporary valve for the catalog backfill — turn it off once the ...
Show whether someone is there, and their extension
Every row now carries a presence dot and word beside the name, and its extension in the detail line — so choosing a person no longer means picking a name and ho...
Carry availability, presence and extension on a directory row
Picking a person is easier when the row says whether they are there and how to reach them, so each one now carries what they last declared, whether anything of ...
Buy a block when an opted-in org runs out of room
Runs shortly after the nightly snapshot sweep, because that is what produces the usage figure it decides on — expanding against yesterday's number would buy a b...
Opt-in auto-expand with a ceiling
Two columns on the subscription that holds the blocks: whether to buy another one automatically when the organization runs out of room, and the most it may ever...
Let a catalog price change actually reach Stripe
Stripe Prices are immutable and the mapping holds one row per (org, type, code, cycle) behind a unique key, so once a price existed it could never change. A pri...
Add the columns that make a price change detectable
Order(0), nullable, no backfill — a null amount reads as "we do not know what this price was created with" and the reconciler treats it as unknown rather than a...
Record what each Stripe price was created with
The mapping stored only which price id to use, so a catalog price change could not even be noticed. ensurePlanPrices short-circuited on the row's existence and ...
Type the storage capacity the summary now returns
Keeps the client contract in step with BillingService, which now reports what the plan includes, what blocks were bought, the combined ceiling and the fraction ...
Report capacity alongside usage
The storage panel reported bytes stored against nothing at all — no allowance, no percentage, no money — so an owner looking at 400 GB had no way to tell whethe...
Give the storage allowance an effect
Storage was measured across nineteen domains, nightly, with a build-failing coverage test — and nothing anywhere computed an allowance or refused a byte. The pl...
Tell the requestor whether their agent is actually there
A silent thread does not distinguish "nobody has looked at this" from "my agent stepped away", and the person waiting cannot tell which. The notice under the co...
Reassignment reaches the new agent, and the requestor is told who is there
Reassignment did not reach anybody. assignmentOpenedAt records that a client put the conversation on screen, and it is what raises a ticket at sign-in — but it ...
Assign a chat request, and assign from the tickets list
Two gaps, same cause — assignment was treated as something that happens to a ticket rather than to a piece of work. The assign button was hidden for pre-ticket...
Assigning by hand must not consume the PRE_TICKET status
assignToAgent and reassignAgent set ASSIGNED unconditionally, which quietly made a chat request impossible to convert afterwards — convertPreTicket accepts noth...
Quote engine for the plan picker, matching what Stripe will bill
The arithmetic behind a live running total, kept apart from any component so the numbers can be tested directly. It mirrors **************** exactly: per-seat a...
Make a first plan purchasable, and return real plan prices
Two things stopped an organization ever buying a first subscription. Plan lists were reachable only through sub.marketId on a subscription the org already held...
Let a sub-org reach the account that pays for it
AccountSubscription deliberately separates the payer from the organization whose members consume the seats, and auto-provisioned accounts sit on the top-level K...
Gate paid actions on a licensed seat, at the chokepoint
Nearly every gateable action in the product — dial an extension, email a member, open the AI assistant, play a recording, start a social conversation — is a bar...
Resolve what a member may do, not just what their plan includes
Feature grants describe a plan. They never described this member, because the console gates navigation on org-level org.features — so a member with no license a...
Derive the add-on unit from the catalog, and correct stale prices
The storage add-on advertised as "+$4 /user/month" for a pool the same page called organization-wide. Cause: mapAddOns matched addonCode "additional_storage" wh...
Bill per-unit add-ons by units bought, not by headcount
Threads the new add-on quantity through create and update: a FLAT add-on now takes its Stripe quantity from what the subscription actually bought, so four 10 GB...
Reprice Additional Storage as $2.50 per 10 GB, org-wide
The add-on described an organization-wide shared pool and was then priced USAGE_BASED at $4 with "25 GB" stated only in prose. Nothing ever submitted a storage ...
Give add-ons a unit size and a purchasable quantity
An add-on's unit only ever existed as prose inside its description ("Each unit adds 25 GB") and as a hardcoded constant in the marketing site, so nothing could ...
Charge what the catalog says, once, for the right quantity
Five defects that all moved real money, plus the sub-resource IDOR beneath them. Billing cycles. ANNUAL prices were sent to Stripe at the stored per-month rate...
Pick the agent to reassign a ticket to, instead of typing their id
Assign and reassign opened a text box you typed a raw member id into — which meant already knowing the id of the person you wanted, for an action whose whole pu...
Validate who a ticket may be handed to by hand
assignToAgent and reassignAgent checked only that the id resolved to a member. Assignment adds that member to the ticket's chat session, so an id from another o...
Design for the platform billing redesign
Records the audit finding that measurement, entitlement and money are three pipelines that were each built well and never connected, plus the 17 confirmed defec...
Verify god mode against the database, not a forged header
AccountOrgScopeInterceptor short-circuited every org and member check on the raw X-God-Mode-Active header, and kamo-internal's billing proxy copies that header ...
Auto Assignment offers the ticket, it does not allocate it
Auto Assignment pages every available support agent and goes to the first one who accepts. It was implemented as "silently pick the least-busy agent who is onli...
Run the topic's strategy for widget chats too
A chat opened in the web widget is the same unit of work as one filed inside the app, but this path skipped the topic's assignment strategy entirely — the only ...
Decide the sign-in auto-open on the server, not in this tab
The per-tab sessionStorage flag was not a record of anything. Signing in on a second device replayed every conversation the first had already popped, and cleari...
Endpoints for the sign-in auto-open handover
GET /sessions/chat/auto-open returns the chat conversations this member's client should raise; POST /sessions/{guid}/auto-opened records that it did. The durabl...
Durable record of a conversation the client raised by itself
MediaSessionMember gains AUTO_OPENED_AT, beside msgIdLastViewed because it is the same grain — a fact about one member's relationship to one session. That one a...
Overhaul the preferences panel
Four dropdowns and a switch on a panel washed top to bottom in the tenant colour at full strength, so every control fought its own background. Each select hid t...
Replace the mailbox spinners with a skeleton that assembles
Every loading state on /messages was a Loader2 on an otherwise empty pane — the boot, the message list, the folder tree, the message body and the next page all ...
Restore the CRLF endings on the calendar types
Same script-normalisation slip as the attendee repository: the file was rewritten through Python, which converted every line ending and turned a handful of type...
Don't let one failed request silence the sign-in auto-open
Unread chats are auto-opened once per tab-session, guarded by a sessionStorage flag. That flag was written up-front, before the fetch that tells a chat session ...
Add and remove guests from the event dialog
The last thing the dialog could not do. It could display who was already on an event, but there was no way to put anyone there — the only guest control was "upd...
Manage an event's guest list
The dialog could show who was already on an event but never add or remove anyone; the only guest operation was re-notifying people who were somehow already ther...
Restore the CRLF endings on the attendee repository
The previous commit rewrote the file through a script that normalised its line endings, so a twelve-line addition arrived as a fifty-line rewrite. That buries t...
Batched guest lookup, and delete-by-event
The event dialog shows who an event is with, and the list endpoint returns a month of events at a time. Asking for guests per event is a query per row; findByEv...
Tell the member what the event collides with, as they set it
The most useful thing a scheduling dialog can say is "you already have something then", and the endpoint to answer it existed all along with nothing calling it....
Let the conflicts endpoint exclude the event being edited
ConflictDetectionService has always taken an excludeEventId; this endpoint passed null. Without it an event overlaps itself, so the moment the event dialog star...
Stop typing an unseen support session as a plain chat
A live activity frame for a session not already in the list synthesized a row hard-coded to 'CHAT'. For a support ticket that put it in the wrong sub-tab, showe...
Put the session type on the activity frame too
The unread/ACTIVITY broadcast is published for every session type and carried no indication of which. The chats list, having nothing to go on, typed an unseen s...
Like what you see shipping?
Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.