AACSearch / guide
Platform operations
The screens only the SaaS team sees — the business overview, the organization directory, the model catalog, search cluster, scheduler and raw resources — plus the readiness matrix and the console's navigation aids.
Platform screens become visible only for accounts the API lists in
PLATFORM_ADMIN_EMAILS. Being an owner of an organization does not grant
them, on any plan; a customer who opens a platform URL sees an explanation
rather than a broken screen, and the API refuses the underlying calls anyway.
The sidebar group appears once the session is recognised as a platform
operator.
Business overview
/dashboard/admin/overview is the platform at a glance — the numbers an owner
opens the admin console for:
- Organizations, paying organizations (active or trialing on a paid plan), users and live API keys across every tenant.
- Signups for the trailing 30 days, per day.
- Wallets — organizations with a wallet, credits held, and the credits moved in the last 30 days split by kind (top-up, usage, adjustment).
- Subscriptions by plan — plan and status across all tenants.
Every figure comes from one aggregate call (admin.overview). Counts are
credits, not currency: the credit-to-currency rate lives in billing policy,
so the page stays unit-honest rather than inventing a revenue number.
Organizations
/dashboard/admin/organizations is the tenant directory — the one view no
customer-facing screen can give, because an operator is not a member of these
organizations and every tenant-scoped route would refuse them.
- List — every organization with plan, subscription status, renewal date, member and live-key counts, wallet balance and signup date. Searchable by name, slug or id, filterable by plan and subscription status, paged.
- Detail (
/dashboard/admin/organizations/{id}) — identity (including RU legal-entity billing fields and the Stripe customer), billing (plan, period, trial, seats, wallet balance, credits burned/topped up, the plan's features), the team with roles, the API keys (prefixes only — key material is hashed at rest), and the last 50 wallet movements. - Suspend / unsuspend — the operator's own control. A customer can suspend only their own organization and only as an owner; no customer-facing screen can suspend a different tenant. Suspension stops their search and API calls until lifted, and the reason is recorded in the platform audit log.
- Adjust credits — refund, goodwill grant or correction, with a required
reason. Idempotent on a reference, so a retried request cannot double-credit;
a negative amount debits (a refund). The adjustment lands in the tenant's
wallet ledger as
adjustmentand in the platform audit log.
Revenue
/dashboard/admin/revenue answers "who paid how much", across every tenant.
Two ledgers, never added. The page keeps two different units apart:
- Cash collected — credits that entered a wallet through a payment provider (a Stripe or Robokassa top-up). This is money that actually arrived.
- Credits burned / held — the internal prepaid unit being consumed. A credit balance is a liability, not revenue, so it is never added to cash.
MRR is monthly-normalised from the platform's plan list and labelled as such. Plan prices live with the provider today, so a plan with no local price reports "—" rather than a guessed number, and the page says how many paying tenants that affects.
Billing issues
/dashboard/admin/billing-issues is the dunning queue: subscriptions in
past_due, unpaid, incomplete, incomplete_expired or canceled, plus the
tenants cancelling at period end — with headline counters for past-due,
trialing and cancelling. Payment methods and the retry action live in Stripe;
each row links to the tenant's Billing tab.
Each organization also has a Billing tab
(/dashboard/admin/organizations/{id}/billing): the Stripe invoices behind the
wallet (paid to date, per-invoice status/amount/period, hosted invoice links)
beside the credit ledger. The invoice list is fetched live — nothing is cached.
Data browser
/dashboard/admin/resources is the god-mode browser: any tenant-scoped row of
any organization. The resource list, its columns and whether it is writable all
come from the API (admin.resources), so the UI never offers a table the API
would refuse.
Its safety rails are structural, not cosmetic:
- Allowlist, not a denylist — a table absent from the registry is a 404. A new table is invisible until deliberately added.
- Secrets are dropped from the query, not hidden in the UI:
hashedKey,signingSecret,publishToken,keyHashand friends never leave the database. - A tenant-scoped resource requires an organization id — a cross-tenant list is refused, so the "no query omits the tenant scope" rule holds by construction.
- Read by default; writes only for resources that opt in, and
id,organization_id,reference_idandcreated_atare immutable so an edit can never re-home a row into another tenant. - Nothing is deletable through god mode: deleting a tenant, its ledger or its keys has an owning workflow (suspend, revoke, offboard).
- Every edit is audited with before/after, the reason, and the operator.
- CSV export goes through the same registry, so a redacted column can never be requested by name. The cap is 10 000 rows, and hitting it is stated in both the response and the file — silent truncation is worse than refusing.
Plans & prices
/dashboard/admin/plans manages the plan catalogue. Price is a version, not a
column: setting a new price adds a row with a new effective date and closes the
previous version's window, so a subscription sold at the old price keeps it —
that is grandfathering, and Stripe makes price amounts immutable for the same
reason. The page shows every version, who set it and why, and never offers to
edit history. Archiving a plan stops new sales without touching existing
subscriptions.
Once a price exists, the Revenue page's MRR becomes a measured number instead of "—".
Tenant health
/dashboard/admin/health ranks tenants by churn risk, from signals already in
the data: never used, idle 30d+, paying with an empty wallet,
past due, and free past the trial window. The score is deliberately
blunt — one signal is 25 points — because a rule an operator can argue with beats
a model they cannot inspect.
Support impersonation
From an organization's page, Impersonate opens a time-boxed support session. It is not a session cookie and does not let an operator become a user: it is a record — org-scoped, 30 minutes by default, read-only by default — and every action taken inside it stays attributed to the operator.
The safeguards, each one a documented failure of the naive version:
- A reason is mandatory and is shown to the tenant.
- Read-only by default;
writemust be asked for and is still refused for money, API keys and access control — minting a key during impersonation is the classic escalation, because the key outlives the session. - Recorded twice on purpose: in the platform audit log and in the organization's own audit log, so the tenant's compliance team can see support signed in. This is the one deliberate exception to "operator actions never enter a tenant's trail".
- Refused for SSO-only or IP-allowlisted organizations unless explicitly overridden — impersonating those would silently defeat a control the tenant set.
- Expiry is the real backstop: a closed tab never calls "end", so an expired session is over regardless of what the client thinks. Sessions can also be revoked in bulk per organization.
Platform audit
/dashboard/admin/platform-audit is the platform team's own trail — what an
operator did to the platform: suspending a tenant, adjusting credits, changing
the model catalog, running a job. It is a separate table
(platform_audit_log) from an organization's audit log, which is why it is
shown here and not under the organization: an operator acting on a tenant is not
a member of it, the action is not the tenant's event to see, and the
organization's audit log is readable by that organization's own members — so
recording an operator there would both mis-attribute the action and leak
operator identities to the customer.
Search cluster
/dashboard/engine is the read-only view of the single search engine behind
the tenant API:
- A health badge from
GET /v1/health, distinguishing healthy, unreachable and reachable-but-not-ready. - Engine statistics from
GET /v1/stats.json— request and latency counters per operation, cache hit ratio, pending write batches — plus per-endpoint latency and throughput breakdowns. An Idle badge appears while every counter is still zero, and an "Updated hh:mm:ss" line marks the last read. - Collection and document totals for the active organization (the card reads the tenant collections endpoint, which is organization-scoped, not platform-wide).
- A connection card showing the API base URL, the
X-NOVA-API-KEYheader and the two key scopes, linking to the organization's API keys and collections. - A header Refresh.
Each of the three requests settles independently, so one failure degrades only its own card. Counters are zero until the node sees traffic.
Maintenance operations that the engine itself exposes — snapshots, cache
clearing, database compaction, raft re-election and schema-change status — are
deliberately not proxied by the tenant API: every caller, platform
operators included, gets 403 — not available through the tenant API. Run them
directly on the engine host.
Scheduler
/dashboard/admin/scheduler shows the platform's cron jobs and their history:
- Jobs — name, schedule (or "every tick"), last run (or "never run"), a status badge (including a destructive marker), and duration. Jobs are listed by name.
- Recent runs — the last 100 runs with when, job, trigger, status, duration and error.
- Run now — triggers a job manually. A manual run is attributed to the caller's active organization for the audit log, so the screen asks for an active organization first; the outcome (the reason a job did not run, or the error) is reported inline.
- A header Refresh and a Raw resources cross-link.
Cloudflare Queue depth is intentionally not shown: a Worker cannot introspect its own queue or dead-letter queue at runtime, and the application holds no account-level token.
Raw resources
/dashboard/admin renders every resource of the OpenAPI contract as plain CRUD
tables for the platform team, using the same session cookie and the active
organization. Unlike the rest of the dashboard it has its own admin chrome: a
slim exit bar labelled "Raw resources · platform team" with a Console link
back to Overview, links to Audit log, Scheduler and Models, and the account
menu. The merchant sidebar steps aside on exactly this route.
- Full create / read / update / delete — collections, documents, aliases, synonym sets, curation sets, stopword sets, presets, NL search models, search rules, conversation models, assistants, recommendation models and analytics rules.
- Create / read / update, no delete — stemming dictionaries, which the API does not allow deleting (re-import the same id to replace one).
- Read-only — synonym suggestions and the entitlement singleton.
- List only — analytics events, which require both a user id and a rule name as filters.
- Experiments also carry a dedicated detail view over the tenant API, for the experiment report and promote actions.
Media and voice are not registered here: they are action endpoints without a "list everything" shape.
Models
/dashboard/admin/models is the model catalog — which language, embedding,
rerank and speech model each role runs, and which is the default for it. It is
platform-wide: the same rows govern every organization, so an organization reads
the catalog and picks from it, while only platform operators may change it
(model.create / model.update / model.delete; a customer's call is refused
with 403).
- Roles —
chat,embedding,vision,rerank,speller,query_rewrite,graph_extract,graph_map,graph_reduce,asr, andconversation. The platform picks the first ten itself; an organization never names them.chatis also an allowlist for assistants: an assistant may only use a model the catalog offers for it.conversationgoverns the engine-side models an organization registers for conversational and NL search (POST /conversations/models,POST /nl_search_models) — those requests are validated against the catalog before they reach the engine. - Add / promote / disable / remove — one entry per role + model id, with at most one default per role (promoting one demotes the previous default in the same transaction). A disabled entry can be neither chosen nor resolved.
- Resolved models — each role's current model and where it came from: Catalog default or Built-in, the platform's own model when the role has no enabled catalog entry.
An empty catalog is deliberately a no-op: nothing is refused, which is the
behaviour that predates the catalog. The migration 0046_seed_model_catalog
seeds every role with the models the API already runs (the same values as its
built-in fallbacks), so a fresh deployment starts with an explicit catalog that
changes no behaviour. Set the API's MODEL_CATALOG_STRICT=1 to close the last
gap: with it, an empty chat catalog refuses an arbitrary model instead of
accepting any string. Populate the roles you intend to offer, then turn it on.
Readiness matrix
/dashboard/capabilities is described in the Console overview.
It is not linked from the sidebar: open the URL directly. A second unlisted
screen, /dashboard/ranking, is the Ranking Studio.
Navigation aids
- Organization switcher (top of the sidebar) — shows the active organization and switches or creates one; switching changes what every screen reads and writes.
- Command palette (⌘K / Ctrl-K, or the search button on mobile) — jumps to any screen, the organization's collections and a few start actions (new collection, import documents, try a search, new API key). It filters out platform-only and admin-only entries the account cannot use, and remembers recently visited pages in the browser.
- Notification bell — the count of open alerts, polled every minute, linking to Alerts & SLOs. Alerts are the only event feed the API exposes; the API has no per-user read state, so "seen" is remembered in the browser.
- Quota and subscription banners — shown above a screen when the API is paused, the quota is exhausted, the wallet is empty, or the burn rate is heading for the limit, and for trial, past-due and canceled subscriptions. The banners quote the same numbers as the Usage and Billing screens and are hidden on those two screens, where the full detail is already visible.