Subscriptions are a fifth, separate thing. They decide the CTA and the
tier badge on a card that is already visible — never visibility itself.
What the hub reads
The hub issues two queries against the TeleQuick portal API.hub.overview({ orgId }) is an org-scoped procedure and returns:
If the
organization row cannot be read, the procedure throws
NOT_FOUND. The hub polls this query on an interval while the page is
open.
traffic.overview({ orgId, windowMinutes }) backs the state strip and
the area chart. The hub calls it with a 1440-minute window. See
Account traffic on the hub.
Only voice, robotics and games come back as verticals[] rows.
The hub derives two headline numbers from them: totalCost (the sum of
cost_mtd_usd across all three) and activeCount (how many have a
non-null subscription).
Why a product card appears
The card catalogue (CARDS) is static in the portal bundle. The hub
filters it:
- The product list wins. If
hub.overviewlisted a product whosekeymatches the card’svertical, the card is shown. - External cards have a fallback. A card marked
externalis shown even when the product list does not mention it — but only on an unrestricted deployment (restricted === false) and only when the card is not markedoptIn.
hub.overview resolves, the hub falls back to the full
catalogue minus mock cards. Mock cards are preview surfaces with no
real backend; they appear only when the build sets VITE_MOCK_SPAS=1,
so a production build never advertises a dummy-data console.
The card metadata that participates in this decision:
Two consequences are worth spelling out, because both have shipped as
bugs:
- A product served on its own subdomain with no billed vertical row
must be
external. The non-external CTA branch keys off averticals[]subscription row, so a non-external card for an unbilled product renders a “Get started” CTA and then callssetSubscriptionwith a vertical that no billing path accepts — the card looks live and does nothing. externalalone re-opens the visibility gate, because external cards fall back to always-visible on an unrestricted deployment. Pair it withoptInwhen the product should only reach entitled orgs.
optIn narrows who sees a card. It never changes where the card’s
button points. A card whose manageHref resolves to a host that serves
nothing will still render and still 404 on click.
Deployment allowlist vs entitlement claim
These are two different gates that both produce theproducts array,
and they answer different questions.
HUB_PRODUCTS is deployment configuration on the portal API. It
describes what this install serves. A single-modality on-prem box pins
it so the hub hides every other product — including external cards,
because a pinned allowlist sets restricted: true and that disables the
external fallback. On cloud the variable is empty, so restricted is
false and external cards always show.
The tenant entitlement claim describes what this org is allowed
to use. The portal API loads it with
getTenantEntitlement(orgId) and passes it to buildHubProducts, which
returns the allowlist filtered by entitlement.
Entitlement resolution is deliberately optimistic:
getTenantEntitlementis wrapped in.catch(() => null). A lookup failure never fails the hub query.- A
nullentitlement — entitlements not configured, or no cert has landed for the tenant yet — is treated as all entitled. - Only an explicit
falseinclaims.modalities[vertical]denies.
Per-user product grants
Deployment and entitlement decide which cards exist for the org. Grants decide which of those cards this member may open. The hub reads them from the tenant context:- Grants are checked at the
viewerlevel, keyed by the card’svertical. activeGrants === nullmeans not loaded yet, not denied. The hub treats an unloaded grant set as granted so that the first paint does not flash an empty account at a member who has access.
Subscriptions, tiers and trials
organization.subscriptions is a JSON column on the org row, keyed by
vertical. Each value is either null (not subscribed) or a record:
hub.overview reads this column and hands each vertical’s record to its
snapshot helper, which returns the card’s status, headline and
cost_mtd_usd. A vertical with no record snapshots as
status: 'not_subscribed' with a zero month-to-date cost.
A subscription record controls the CTA and tier badge on a card. It does
not control whether the card appears — that is the allowlist, the
entitlement claim and the grant, in that order. Likewise, an external
card has no subscription record at all, so its tier badge is replaced by
externalLabel and its CTA is always the console link.
Note that one plan can back more than one card. The Voice AI card
carries the billed voice subscription row; the Contact Center console
is a second surface of that same plan and is modelled as an external
card with its own vertical key so it does not collide with the billed
row in per-card lookups.
Activating and unsubscribing a product
Clicking Get started on a billable card callshub.setSubscription. It is an org admin procedure — owner or admin
only. Plain org membership is not sufficient, because this mutation
changes the org’s billing.
Input:
The mutation runs in this order:
- Entitlement gate. When
tier !== null, the entitlement is loaded. Ifclaims.modalities[vertical]is explicitlyfalse, the call fails withFORBIDDENand the message “Your plan does not include the … product. Upgrade to enable it.” Anullentitlement is optimistic and does not block. - Read the current record. The org row is fetched; a missing row
throws
NOT_FOUND. - Compute the next record.
tier: null→ the vertical’s value becomesnull.- Same tier as the existing record → the record is returned unchanged,
so
activated_atandtrial_ends_atsurvive a no-op tier change. - Otherwise a new record is written: the supplied
tier,activated_atcarried over from the previous record if one existed (else now), andtrial_ends_atset to now plustrialDaysdays whentrialDays > 0, elsenull.
- Write it back. The new value is merged into the existing
subscriptionsobject; other verticals are untouched. A write failure throwsINTERNAL_SERVER_ERROR. - Audit. An audit entry is recorded — action
subscribeorunsubscribe, resource typeorg_subscription, resource id the vertical, with the full before and after records.
{ ok: true, subscriptions } with the complete
updated map, and the card transitions from Get started to
Manage.
Subdomain vs single-host path routing
Each card’s CTA is built by the portal’ssubdomain(prefix, path)
helper. It has three modes, checked in this order.
1. Single-host path mode. When runtime config sets
MODALITY_MODE=path, the link is same-origin:
<modality>.<brand>
subdomains do not exist. The prefix-to-path-segment map is:
A prefix with no entry falls through to itself. Note that
optimus and
arena both map to fleet: one bundle serves both the robotics and
games verticals.
2. Loopback development. On localhost, 127.0.0.1 or a
*.localhost host, the link points at the sibling SPA’s Vite dev server
port instead. This mapping exists because the brand-host helper parses
the registrable domain off window.location.host with a TLD regex that
a trailing :port defeats — without the loopback branch every card in a
dev stack would silently link to the production site. The fleet bundle
picks its vertical off the hostname, which one loopback origin cannot
express, so the robotics and games cards append a ?vertical= override
in this mode.
3. Subdomain (default, cloud).
https://<prefix>.<brand-host>/<path>. The brand host is the
registrable domain of the page the user is currently on, so a card’s
Manage button stays on the brand the user arrived from rather than
hardcoding one of them.
If a card’s CTA 404s, the cause is in this helper or in what the target
host serves — not in the visibility gates above.
Recent activity and the audit log
hub.overview returns recentActivity: the five most recent rows of
public.audit_log for the org, ordered newest first, selecting ts,
actor_label, action, resource_type and resource_id.
This is the admin-configuration audit log. Subscription changes land in
it directly — hub.setSubscription records an entry on every successful
write, with the actor’s user id and email label, so a tier flip or an
unsubscribe is attributable after the fact and carries both the before
and after subscription records.
The hub shows only the newest five rows. It is a recency feed, not a
searchable history.
Account traffic on the hub
The state strip and area chart come fromtraffic.overview, which the
hub calls with windowMinutes: 1440.
The procedure unions the engine meter rollup with the QUIC sampler
rollup in ClickHouse, re-aggregates to one row per modality and 1-minute
bucket, and returns:
kpi—bytes_in,bytes_out,bytes_total,avg_mbps,peak_mbps,sessions,modalities,window_minutes.byModality— per-modality totals sorted by bytes.series— one point per 1-minute bucket withbytesIn,bytesOutand abyModalitybreakdown.
avg_mbps stays honest when traffic spans only part of the window.
peak_mbps is the busiest single 1-minute bucket expressed over its 60
seconds.
ClickHouse failures soft-empty. If the rollups are unreachable or
not yet migrated, the procedure returns the zeroed shape instead of
throwing. The hub therefore cannot distinguish “nothing reported in the
window” from “the rollup is degraded” — both yield
kpi.modalities === 0 — so it renders an em dash rather than a
confident 0 B. A degraded rollup costs the hub a dash, never the page.
The hub shows the account total only. byModality is present in the
response, but its keys are transport subjects (voice_rtp, sip,
webrtc, media, wt_media, robot, match_player, …), not the
product verticals the cards represent. Several subjects map to more than
one product and voice cannot be separated from contact centre at all, so
a per-card byte count would be a guess presented as a measurement. The
total is exact — the union counts every modality once — and per-product
detail belongs on the Traffic screen, which is built for it.
Related
- Authentication — API keys, relay tokens and the org scoping the hub queries use
- Telemetry — the metric and CDR streams behind the traffic rollups