RTK Query helpers over the typed Norbix SDK — cache, dedup, refetch, invalidation.
@norbix.ai/react-redux is a thin, opinionated layer that wires the Norbix TypeScript SDK into a Redux Toolkit + RTK Query app. You get React hooks for the most-used Norbix endpoints with full caching, request dedup, and tag-based invalidation built in. Under the hood every hook calls the same typed Norbix SDK — so DTOs, auth, errors, and base URLs all stay consistent with the rest of your stack.
| You get | Provided by |
|---|---|
useGetUsersQuery, useFindCollectionQuery, useInsertOneMutation, ... |
this package |
| Cache, dedup, polling, refetch on focus, optimistic updates | RTK Query (@reduxjs/toolkit/query) |
| Tree-shakeable typed methods, JWT auth, error mapping | @norbix.ai/ts SDK |
npm install @norbix.ai/react-redux @norbix.ai/ts @reduxjs/toolkit react-redux reactThree steps: create the API slice, plug it into your store, mount the provider.
// src/norbix.ts
import { Norbix } from '@norbix.ai/ts';
import { createNorbixApi } from '@norbix.ai/react-redux';
export const norbix = new Norbix(); // reads env vars, or pass { apiKey, projectId }
export const norbixApi = createNorbixApi(() => norbix);
export const {
useGetUsersQuery,
useInviteUserMutation,
useFindCollectionQuery,
useInsertOneMutation,
useUpdateOneMutation,
useDeleteOneMutation,
useGetAccountProfileQuery,
useGetDatabaseSchemasQuery,
useLoginMutation,
} = norbixApi;// src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { norbixApi } from './norbix';
export const store = configureStore({
reducer: { [norbixApi.reducerPath]: norbixApi.reducer },
middleware: (getDefault) => getDefault().concat(norbixApi.middleware),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;// src/main.tsx
import { Provider } from 'react-redux';
import { NorbixProvider } from '@norbix.ai/react-redux';
import { store } from './store';
import { norbix } from './norbix';
createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<NorbixProvider client={norbix}>
<App />
</NorbixProvider>
</Provider>,
);// src/UsersList.tsx
import { useGetUsersQuery, useInviteUserMutation } from './norbix';
export function UsersList() {
const { data, isLoading, error } = useGetUsersQuery({ pageSize: 20 });
const [invite, { isLoading: inviting }] = useInviteUserMutation();
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data?.list?.items.map((u) => (
<li key={u.id}>{u.email}</li>
))}
<button disabled={inviting} onClick={() => invite({ email: 'maya@team.io' })}>
Invite
</button>
</ul>
);
}After invite() resolves, the User/LIST tag is invalidated and useGetUsersQuery automatically refetches — no manual cache busting needed.
A curated set of hooks for the most common endpoints. Every hook is end-to-end typed from the SDK's own DTOs.
| Hook | Wraps | Cache tags |
|---|---|---|
useLoginMutation |
client.login(...) |
invalidates User/LIST, AccountProfile/CURRENT |
useLogoutMutation |
client.logout() |
invalidates User, AccountProfile, Collection, Schema |
useAuthenticateMutation |
api.auth.authenticate |
— |
useGetUsersQuery |
api.membership.getUsers |
provides User/LIST |
useGetUserQuery |
api.membership.getUser |
provides User/<id> |
useInviteUserMutation |
api.membership.inviteUser |
invalidates User/LIST |
useUpdateUserMutation |
api.membership.updateUser |
invalidates User/<id> + User/LIST |
useBlockUserMutation |
api.membership.blockUser |
invalidates User/LIST |
useUnblockUserMutation |
api.membership.unblockUser |
invalidates User/LIST |
useDeleteUserMutation |
api.membership.deleteUser |
invalidates User/LIST |
useFindCollectionQuery |
api.database.find |
provides Collection/<name> |
useFindOneQuery |
api.database.findOne |
provides Collection/<name> |
useCountCollectionQuery |
api.database.count |
provides Collection/<name> |
useInsertOneMutation |
api.database.insertOne |
invalidates Collection/<name> |
useUpdateOneMutation |
api.database.updateOne |
invalidates Collection/<name> |
useReplaceOneMutation |
api.database.replaceOne |
invalidates Collection/<name> |
useDeleteOneMutation |
api.database.deleteOne |
invalidates Collection/<name> |
useFindTermsQuery |
api.database.findTerms |
provides DatabaseTaxonomyTerms/<taxonomyName> |
useFindTermsChildrenQuery |
api.database.findTermsChildren |
provides DatabaseTaxonomyTerms/<taxonomyName> |
useFindTermTreeQuery |
api.database.findTermTree |
provides DatabaseTaxonomyTerms/<taxonomyName> |
useFindTaxonomyTreeQuery |
api.database.findTaxonomyTree |
provides DatabaseTaxonomyTerms/ANY |
useGetApiKeysQuery |
api.apikeys.getApiKeys |
provides ApiKey/LIST |
useRegenerateApiKeysMutation |
api.apikeys.regenerateApiKeys |
invalidates ApiKey/LIST |
useGetPublicFileQuery |
api.files.getPublicFile |
provides Files/PUBLIC:<publicId> |
useTestFilesIntegrationApiMutation |
api.files.testFilesIntegration |
invalidates Files + FilesIntegrations |
useCreatePaymentOrderMutation |
api.payments.createPaymentOrder |
invalidates PaymentOrders/LIST |
useGetPaymentOrderQuery |
api.payments.getPaymentOrder |
provides PaymentOrders/<id> |
useGetPaymentOrdersQuery |
api.payments.getPaymentOrders |
provides PaymentOrders/LIST |
useStartPaymentCheckoutMutation |
api.payments.startPaymentCheckout |
invalidates PaymentOrders/LIST + PaymentOrders/<id> |
| Hook | Wraps | Cache tags |
|---|---|---|
useGetAccountProfileQuery |
hub.account.getAccountProfile |
provides Account |
useGetAccountStatusQuery |
hub.account.getAccountStatus |
provides Account |
useUpdateAccountProfileMutation |
hub.account.updateAccountProfile |
invalidates Account |
useGetProjectsQuery |
hub.account.getProjects |
provides Projects |
useGetProjectQuery |
hub.account.getProject |
provides Projects |
useCreateProjectMutation |
hub.account.createProject |
invalidates Projects |
useDeleteProjectMutation |
hub.account.deleteProject |
invalidates Projects |
useListRegionsQuery |
hub.regions.list |
provides Regions |
useUpdateProjectRegionsMutation |
hub.regions.updateProjectRegions |
invalidates Regions + Projects |
useGetDatabaseSchemasQuery |
hub.database.getDatabaseSchemas |
provides Schema/LIST |
useGetDatabaseSchemaQuery |
hub.database.getDatabaseSchema |
provides Schema/<id> |
useSaveDatabaseSchemaMutation |
hub.database.saveDatabaseSchema |
invalidates Schema/LIST |
useRenameDatabaseSchemaMutation |
hub.database.renameDatabaseSchema |
invalidates Schema/LIST + Schema/<id> |
usePublishDatabaseSchemaMutation |
hub.database.publishDatabaseSchema |
invalidates Schema + Collection |
useDeleteDatabaseSchemaMutation |
hub.database.deleteDatabaseSchema |
invalidates Schema/LIST + Collection |
useGetEmailTemplatesQuery |
hub.notifications.getEmailTemplates |
provides Notification/EMAIL_TEMPLATE_LIST |
useGetEmailTemplateQuery |
hub.notifications.getEmailTemplate |
provides Notification/EMAIL_TEMPLATE/<id> |
useCreateEmailTemplateMutation |
hub.notifications.createEmailTemplate |
invalidates Notification/EMAIL_TEMPLATE_LIST |
useUpdateEmailTemplateMutation |
hub.notifications.updateEmailTemplate |
invalidates list + per-id |
useDeleteEmailTemplateMutation |
hub.notifications.deleteEmailTemplate |
invalidates list |
useTestFilesIntegrationMutation |
hub.files.testFilesIntegration |
— |
useMakeFilePublicMutation |
hub.files.makeFilePublic |
invalidates Files |
useMakeFilePrivateMutation |
hub.files.makeFilePrivate |
invalidates Files |
useMakeFolderPublicMutation |
hub.files.makeFolderPublic |
invalidates Files |
useMakeFolderPrivateMutation |
hub.files.makeFolderPrivate |
invalidates Files |
Need a hook we don't ship? Two options. (1) Drop down to the SDK with
useNorbix()for a one-off call. (2) Add a new file undersrc/hooks/api/orsrc/hooks/hub/, follow the pattern of the others, and open a PR.
Every hub.database method (73) and every api.database method (22) of @norbix.ai/ts has a hook; Hub hooks keep the SDK method name. The rows below are the ones added for @norbix.ai/ts >= 4.9.0.
| Hook | SDK method | Cache |
|---|---|---|
useFindRecordsQuery |
hub.database.findRecords |
provides DatabaseRecords/<collectionName> |
useFindOneRecordQuery |
hub.database.findOneRecord |
provides DatabaseRecords/<collectionName> |
useCountRecordsQuery |
hub.database.countRecords |
provides DatabaseRecords/<collectionName> |
useDistinctRecordValuesQuery |
hub.database.distinctRecordValues |
provides DatabaseRecords/<collectionName> |
useAggregateRecordsMutation |
hub.database.aggregateRecords |
— (never cached) |
useExecuteRecordsAggregateMutation |
hub.database.executeRecordsAggregate |
— (never cached) |
useGetCollectionIndexesQuery |
hub.database.getCollectionIndexes |
provides DatabaseCollections/<collectionName> |
useInsertRecordMutation |
hub.database.insertRecord |
invalidates DatabaseRecords/<collectionName> |
useInsertManyRecordsMutation |
hub.database.insertManyRecords |
invalidates DatabaseRecords/<collectionName> |
useUpdateOneRecordMutation |
hub.database.updateOneRecord |
invalidates DatabaseRecords/<collectionName> |
useUpdateManyRecordsMutation |
hub.database.updateManyRecords |
invalidates DatabaseRecords/<collectionName> |
useReplaceRecordMutation |
hub.database.replaceRecord |
invalidates DatabaseRecords/<collectionName> |
useDeleteRecordMutation |
hub.database.deleteRecord |
invalidates DatabaseRecords/<collectionName> |
useDeleteManyRecordsMutation |
hub.database.deleteManyRecords |
invalidates DatabaseRecords/<collectionName> |
useChangeRecordResponsibilityMutation |
hub.database.changeRecordResponsibility |
invalidates DatabaseRecords/<collectionName> |
useSeedCollectionRecordsMutation |
hub.database.seedCollectionRecords |
invalidates DatabaseRecords |
useGetDatabaseTaxonomyTreeQuery |
hub.database.getDatabaseTaxonomyTree |
provides DatabaseTaxonomies + DatabaseTaxonomyTerms |
useGetDatabaseTaxonomyTermTreeQuery |
hub.database.getDatabaseTaxonomyTermTree |
provides DatabaseTaxonomyTerms/<taxonomyName> |
useGetDatabaseMergedTermTreeQuery |
hub.database.getDatabaseMergedTermTree |
provides DatabaseTaxonomyTerms/<taxonomyName> |
useApplyDatabaseSchemaBundleMutation |
hub.database.applyDatabaseSchemaBundle |
invalidates DatabaseSchemas + taxonomies + terms |
useUpdateDatabaseSchemaEmbedMutation |
hub.database.updateDatabaseSchemaEmbed |
invalidates DatabaseSchemas |
useGetDatabaseSchemaListSettingsQuery |
hub.database.getDatabaseSchemaListSettings |
provides DatabaseSchemas |
useGetDatabaseSchemaIndexStatusQuery |
hub.database.getDatabaseSchemaIndexStatus |
provides DatabaseSchemas/<id> — the last schema-index run (building / ready / refused / partial) |
useUpdateDatabaseSchemaListSettingsMutation |
hub.database.updateDatabaseSchemaListSettings |
invalidates DatabaseSchemas |
useGetCollectionImportsQuery |
hub.database.getCollectionImports |
provides DatabaseImports |
useGetCollectionImportQuery |
hub.database.getCollectionImport |
provides DatabaseImports |
useCreateCollectionImportMutation |
hub.database.createCollectionImport |
invalidates DatabaseImports + DatabaseRecords/<collectionName> |
useDeleteCollectionImportMutation |
hub.database.deleteCollectionImport |
invalidates DatabaseImports |
useAnalyzeImportFileMutation |
hub.database.analyzeImportFile |
— |
useRequestImportUploadUrlMutation |
hub.database.requestImportUploadUrl |
— |
useFindOwnQuery |
api.database.findOwn |
provides DatabaseCollections/<collectionName> |
useFindMergedTermTreeQuery |
api.database.findMergedTermTree |
provides DatabaseTaxonomyTerms/<taxonomyName> |
The module enable / disable hooks were always mutations, so nothing changes in your code. From @norbix.ai/ts 4.10.0 they send PUT instead of GET on the wire, matching the gateway:
| Hooks | Route | Cache |
|---|---|---|
useEnableDatabaseMutation, useDisableDatabaseMutation |
PUT /{version}/database/enable|disable |
invalidates Database |
useEnableFilesMutation, useDisableFilesMutation |
PUT /{version}/files/enable|disable |
invalidates Files |
useEnableLoggingMutation, useDisableLoggingMutation |
PUT /{version}/logs/enable|disable |
invalidates Logs |
useEnablePaymentsMutation, useDisablePaymentsMutation |
PUT /{version}/payments/enable|disable |
invalidates Payments |
useGetPaymentsWebhookLogQuery |
GET /{version}/payments/webhooks/log |
provides PaymentIntegrations |
useEnableMembershipMutation, useDisableMembershipMutation |
PUT /{version}/membership/enable|disable |
invalidates Membership |
useEnableEmailMutation, useDisableEmailMutation |
PUT /{version}/notifications/email/enable|disable |
invalidates Emails |
useEnableSmsMutation, useDisableSmsMutation |
PUT /{version}/notifications/sms/enable|disable |
invalidates Sms |
useEnablePushMutation, useDisablePushMutation |
PUT /{version}/notifications/push/enable|disable |
invalidates Push |
The gateway still answers the old GET on these paths for now (it logs a deprecation warning) and will drop it after gateway 0.2, so upgrade the core SDK before then. The scheduler switch moved to PUT earlier (core SDK 4.6.0). The code module (hub.code) has no hooks in this package yet; call useNorbix().hub.code.enableCode(...) directly.
Every hub.logs method has a hook, and api.logs.writeLogs writes your app's own lines.
| Hook | SDK method | Cache |
|---|---|---|
useGetLogsQuery |
hub.logs.getLogs |
provides Logs |
useGetLogQuery |
hub.logs.getLog ({ id }) |
provides Logs |
useGetLogsByCorrelationIdQuery |
hub.logs.getLogsByCorrelationId |
provides Logs |
useCleanLogsMutation |
hub.logs.cleanLogs |
invalidates Logs |
useGetLogSettingsQuery |
hub.logs.getLogSettings |
provides LogsSettings |
useSaveLogSettingsMutation |
hub.logs.saveLogSettings |
invalidates LogsSettings |
useTestLoggingIntegrationMutation |
hub.logs.testLoggingIntegration |
invalidates LogsIntegrations |
useWriteLogsMutation |
api.logs.writeLogs |
invalidates Logs |
useSaveLogSettingsMutation changes only the settings you send; a field left out keeps its value. After
useSaveLoggingIntegrationMutation, call useTestLoggingIntegrationMutation with the integration id —
nothing is delivered to a logging integration until its last test passed. useWriteLogsMutation takes
{ entries } (1–100 lines: title required, level, message, eventCode, correlationId,
timestamp, meta); on a 429 CM-ERRORS-LOGGING-003 its error.retryAfterSeconds says how many seconds
to wait before sending again. useEndUserOwnRecordsReportMutation (hub.database.endUserOwnRecordsReport,
new in the same core SDK) lists every schema's end-user-own-records switch, or turns it on with dryRun: false.
useUpdateManyRecordsMutation / useDeleteManyRecordsMutation (Hub) and useUpdateManyMutation / useDeleteManyMutation (Api) take a new optional allRecords argument. An empty filter ({}, or no filter on update) matches the whole collection and the gateway refuses it with CM-ERRORS-DATABASE-037 unless you pass allRecords: true:
const [deleteMany] = useDeleteManyRecordsMutation();
await deleteMany({ collectionName: 'orders', filter: '{}', allRecords: true });Other gateway rules that now show up as errors from these hooks: an update body with $ operators ($inc, $set, …) is refused with CM-ERRORS-DATABASE-035 (send the plain fields to set); a broken insert or replace document answers CM-ERRORS-DATABASE-036; soft-deleted records are "not found" for update, replace and change-owner, and a bulk update skips them. A caller with only own-record rights may now use the bulk hooks; they touch only that caller's records.
useGetDatabaseSchemasQuery returns only the schemas of the environment the request targets: the env argument, or else the client's environment (norbix.setEnvironment(...), sent as the norbix-env header), or PROD when neither is set. Each row carries env. The schema list settings (useGetDatabaseSchemaListSettingsQuery / useUpdateDatabaseSchemaListSettingsMutation) are stored per environment too.
RTK Query keys its cache by endpoint + arguments, and the client's environment is not an argument. So either pass env in the arguments — each environment then has its own cache entry — or reset the cache when you switch:
// Per call: TEST and PROD cache apart.
const { data } = useGetDatabaseSchemasQuery({ env: 'TEST' });
// Or switch the whole client and drop what was cached for the old environment.
norbix.setEnvironment('TEST');
dispatch(norbixApi.util.invalidateTags(['DatabaseSchemas']));The paging cursors of the schema list (startingAfter / endingBefore) are schema view ids (sch_…). A cursor saved against an older gateway will not match; start the list again from the first page.
For a signed-in project user. useStartEndUserChatTurnMutation answers at once with a turnId; the answer streams on the user's SSE channel ai-chat:{projectId}:{authId} — open it with norbix.aiChat({ authId }) from @norbix.ai/ts (a foreign channel is refused with 403 AiChatChannelRefused, no retry). Needs @norbix.ai/ts >= 4.3.0.
| Hook | SDK method | Cache |
|---|---|---|
useGetEndUserChatAvailabilityQuery |
api.ai.getEndUserChatAvailability |
provides AiChat |
useListEndUserChatSessionsQuery |
api.ai.listEndUserChatSessions |
provides AiChat |
useCreateEndUserChatSessionMutation |
api.ai.createEndUserChatSession |
invalidates AiChat |
useGetEndUserChatSessionQuery |
api.ai.getEndUserChatSession |
provides AiChat |
useRenameEndUserChatSessionMutation |
api.ai.renameEndUserChatSession |
invalidates AiChat |
useDeleteEndUserChatSessionMutation |
api.ai.deleteEndUserChatSession |
invalidates AiChat |
usePinEndUserChatSessionMutation |
api.ai.pinEndUserChatSession |
invalidates AiChat |
useArchiveEndUserChatSessionMutation |
api.ai.archiveEndUserChatSession |
invalidates AiChat |
useGetEndUserChatEntriesQuery |
api.ai.getEndUserChatEntries |
provides AiChat |
useSetEndUserChatEntryFeedbackMutation |
api.ai.setEndUserChatEntryFeedback |
invalidates AiChat |
useListEndUserChatAttachmentsQuery |
api.ai.listEndUserChatAttachments |
provides AiChat |
useUploadEndUserChatAttachmentMutation |
api.ai.uploadEndUserChatAttachment |
invalidates AiChat |
useDeleteEndUserChatAttachmentMutation |
api.ai.deleteEndUserChatAttachment |
invalidates AiChat |
useListEndUserChatMemoryQuery |
api.ai.listEndUserChatMemory |
provides AiChat |
useForgetEndUserChatMemoryMutation |
api.ai.forgetEndUserChatMemory |
invalidates AiChat |
useStartEndUserChatTurnMutation |
api.ai.startEndUserChatTurn |
invalidates AiChat |
Project AI settings, assistants, usage, Admin Portal — norbix.hub.account / embeddings — norbix.hub.ai
| Hook | SDK method | Cache |
|---|---|---|
useGetProjectAiSettingsQuery |
hub.account.getProjectAiSettings |
provides Projects |
useUpdateProjectAiSettingsMutation |
hub.account.updateProjectAiSettings |
invalidates Projects |
useCreateProjectAiAssistantMutation |
hub.account.createProjectAiAssistant |
invalidates Projects |
useUpdateProjectAiAssistantMutation |
hub.account.updateProjectAiAssistant |
invalidates Projects |
useDeleteProjectAiAssistantMutation |
hub.account.deleteProjectAiAssistant |
invalidates Projects |
useGetProjectAiUsageQuery |
hub.account.getProjectAiUsage |
provides Projects |
useSetAdminPortalEnabledMutation |
hub.account.setAdminPortalEnabled |
invalidates Projects |
useGetEmbeddingIntegrationsQuery |
hub.ai.getEmbeddingIntegrations |
provides Ai |
useSaveEmbeddingIntegrationMutation |
hub.ai.saveEmbeddingIntegration |
invalidates Ai |
useGetEmbeddingIntegrationQuery |
hub.ai.getEmbeddingIntegration |
provides Ai |
useDeleteEmbeddingIntegrationMutation |
hub.ai.deleteEmbeddingIntegration |
invalidates Ai |
useTestEmbeddingIntegrationMutation |
hub.ai.testEmbeddingIntegration |
invalidates Ai |
useSetLlmIntegrationAsDefaultMutation |
hub.ai.setLlmIntegrationAsDefault |
invalidates Ai |
Every call takes the projectId (or the norbix-project-id header your client sets) plus the field it changes. Queries provide Projects; mutations invalidate Projects, so an open useGetProjectQuery refetches after a change. AI settings, assistants and usage are in the table above; LLM / MCP integrations are hub.ai (useGetLlmIntegrationsQuery, useSaveLlmIntegrationMutation, useSetLlmIntegrationAsDefaultMutation, …). Regions: useUpdateProjectRegionsMutation is served by hub.regions (it is spread after hub.account).
| Hook | SDK method | Cache |
|---|---|---|
useGetProjectsQuery |
hub.account.getProjects |
provides Projects |
useGetProjectQuery |
hub.account.getProject |
provides Projects |
useGetProjectTokensQuery |
hub.account.getProjectTokens |
provides Projects |
useCreateProjectMutation |
hub.account.createProject |
invalidates Projects |
useEnableProjectMutation |
hub.account.enableProject |
invalidates Projects |
useDisableProjectMutation |
hub.account.disableProject |
invalidates Projects |
useDeleteProjectMutation |
hub.account.deleteProject |
invalidates Projects |
useUpdateProjectNameMutation |
hub.account.updateProjectName |
invalidates Projects |
useUpdateProjectDescriptionMutation |
hub.account.updateProjectDescription |
invalidates Projects |
useUpdateProjectUrlMutation |
hub.account.updateProjectUrl |
invalidates Projects |
useUpdateProjectLogoMutation |
hub.account.updateProjectLogo |
invalidates Projects |
useUpdateProjectIconMutation |
hub.account.updateProjectIcon |
invalidates Projects |
useUpdateProjectMainColorMutation |
hub.account.updateProjectMainColor |
invalidates Projects |
useUpdateProjectAccentColorMutation |
hub.account.updateProjectAccentColor |
invalidates Projects |
useUpdateProjectAllowedOriginsMutation |
hub.account.updateProjectAllowedOrigins |
invalidates Projects |
useUpdateProjectLanguagesMutation |
hub.account.updateProjectLanguages |
invalidates Projects |
useUpdateProjectDefaultLanguageMutation |
hub.account.updateProjectDefaultLanguage |
invalidates Projects |
useUpdateProjectAdminUrlMutation |
hub.account.updateProjectAdminUrl |
invalidates Projects |
useUpdateProjectLegalDocumentsMutation |
hub.account.updateProjectLegalDocuments |
invalidates Projects |
useUpdateProjectExposeLegalMutation |
hub.account.updateProjectExposeLegal |
invalidates Projects |
useGetAdminPortalStructureQuery |
hub.account.getAdminPortalStructure |
provides Projects |
useAssignAdminPortalServiceUserMutation |
hub.account.assignAdminPortalServiceUser |
invalidates Projects |
useCreateAiServiceUserMutation |
hub.account.createAiServiceUser |
invalidates Projects |
useListAiServiceUsersQuery |
hub.account.listAiServiceUsers |
provides Projects |
useDeleteAiServiceUserMutation |
hub.account.deleteAiServiceUser |
invalidates Projects |
useRotateAiServiceUserKeyMutation |
hub.account.rotateAiServiceUserKey |
invalidates Projects |
useRevokeAiServiceUserKeyMutation |
hub.account.revokeAiServiceUserKey |
invalidates Projects |
useCreateAiServiceUserMutation and useRotateAiServiceUserKeyMutation return the new key once — show it, then drop it from state. The developer MCP endpoint (hub.account.mcp, /account/mcp) is a JSON-RPC stream for MCP clients and has no hook; call it with useNorbix() if you need it.
const { data: project } = useGetProjectQuery({ projectId });
const [setOrigins] = useUpdateProjectAllowedOriginsMutation();
await setOrigins({ projectId, origins: ['https://app.example.com'] }).unwrap();| Hook | SDK method | Cache |
|---|---|---|
useGetPublicProjectConfigQuery |
api.public.getPublicProjectConfig |
provides Config/PUBLIC |
useGetPublicProjectLegalQuery |
api.public.getPublicProjectLegal |
provides Config/LEGAL:<kind> |
The legal document is served only when the project exposes it (useUpdateProjectExposeLegalMutation).
A file, or a whole folder prefix, can be made readable by anyone holding its link. Publishing is a Hub action and needs the signed-in client; reading the link needs nothing at all.
const [publish] = useMakeFilePublicMutation();
const [unpublish] = useMakeFilePrivateMutation();
await publish({ filesIntegrationId: 'nbin_1', path: 'docs/invoice.pdf' });
// `name` is the file's name for a file link, or the path inside the folder
// for a folder link — its slashes stay slashes.
const { data: bytes } = useGetPublicFileQuery({
publicId: 'nbpf_abc',
name: '2026/q1/report.pdf',
});useMakeFilePrivateMutation is refused while a folder above the file is
public — switch the folder off with useMakeFolderPrivateMutation instead.
The four publish hooks invalidate the Files tag, so a listing re-reads and
picks up the new isPublic / publicUrl on each file and the publicFolders
on the page. useGetPublicFileQuery caches under its own
Files/PUBLIC:<publicId> id, so two different links never share one entry;
making a file private again invalidates Files, a different tag, so call
refetch() if a stale entry would matter to you.
Two hooks run the same live probe against a files integration — upload a
small file, read it, list the folder, delete the file — and answer one item
per step (UploadFile, GetFile, GetAllFiles, DeleteFile) with result
OK, FAILED or NOT_TESTED:
| Hook | Wraps | Route | Use it from |
|---|---|---|---|
useTestFilesIntegrationApiMutation |
api.files.testFilesIntegration |
POST /{version}/files/{filesIntegrationId}/test |
your app, with an API key or a user session (needs files:create) |
useTestFilesIntegrationMutation |
hub.files.testFilesIntegration |
POST /{version}/files/integrations/test |
a dashboard, signed in to the Hub |
const [testIntegration, { data, isLoading }] = useTestFilesIntegrationApiMutation();
await testIntegration({ filesIntegrationId: 'nbin_1' });
const failed = data?.items?.filter((i) => i.result !== 'OK');The API one carries the Api suffix because both live in one endpoint map
(like useDeleteFileApiMutation). It invalidates Files — the probe writes to
the storage, and a failed delete step leaves its file behind — and
FilesIntegrations, because the gateway records the last test result on the
integration.
Every SMS Hub endpoint (34) has a hook, 1:1 with norbix.hub.notifications.
Queries provide a tag, mutations invalidate it, so a list re-reads after a
write without any manual refetch:
| Area | Hooks | Cache tag |
|---|---|---|
| Module switch | useEnableSmsMutation, useDisableSmsMutation, useGetSmsDisableDependenciesQuery (what a switch-off would affect — read it before disabling), useGetSmsSettingsQuery |
Sms, SmsSettings |
| Integrations (providers) | useGetSmsIntegrationsQuery, useGetSmsIntegrationQuery, useSaveSmsIntegrationMutation, useTestSmsIntegrationMutation, useConfirmSmsIntegrationHumanDeliveryMutation, useDeleteSmsIntegrationMutation, useSetSmsIntegrationAsDefaultMutation, useEnableSmsIntegrationMutation, useDisableSmsIntegrationMutation |
SmsIntegrations |
| Templates | useGetSmsTemplatesQuery, useGetSmsTemplateQuery, useCreateSmsTemplateMutation, useUpdateSmsTemplateMutation, useDeleteSmsTemplateMutation, useArchiveSmsTemplateMutation, useUnArchiveSmsTemplateMutation, useCloneSmsTemplateMutation, useGetSmsMessageContentTokensQuery, useRenderSmsMutation |
SmsTemplates, Sms |
| Campaigns | useGetSmsCampaignsQuery, useCreateSmsCampaignMutation, useGetSmsCampaignQuery, useDeleteSmsCampaignMutation, useStopSmsCampaignMutation, useGetSmsCampaignStatisticsQuery, useGetSmsCampaignBatchesQuery, useGetSmsCampaignBatchNotificationsQuery, useGetSmsCampaignBatchNotificationQuery, useGetSmsCampaignMessagesQuery |
SmsCampaigns |
| Preview | usePreviewSmsNotificationQuery (opens with the signed link hash, no sign-in — see above) |
Sms |
const [stop, { isLoading }] = useStopSmsCampaignMutation();
const [render] = useRenderSmsMutation();
// Stop a running campaign — no further messages go out; this cannot be undone.
await stop({ id: 'cmp_1' });
// Render a template's Razor code with token values before saving it.
const { data } = await render({
code: 'Hi @Model.FirstName, your code is @Model.Code',
tokens: [
{ name: 'FirstName', value: 'Ada' },
{ name: 'Code', value: '4242' },
],
});useRenderSmsMutation saves nothing on the server, so it invalidates nothing;
it is a mutation only so a render is never served from the cache. The
integration hooks can also be produced by buildIntegrationsEndpoints (next
section) — the shipped ones above are the typed variants.
An "Account users" SMS campaign (accountUsers on useCreateSmsCampaignMutation,
the ids of the account owner / team members) sends to each member's own phone.
A member reads and sets it with two hooks; members without a phone are skipped.
| Hook | SDK method | Wire | Cache |
|---|---|---|---|
useGetMyAccountUserProfileQuery |
hub.account.getMyAccountUserProfile |
GET /account/me |
provides Account |
useUpdateMyAccountUserPhoneMutation |
hub.account.updateMyAccountUserPhone |
PUT /account/me/phone |
invalidates Account |
const { data } = useGetMyAccountUserProfileQuery();
const [setPhone] = useUpdateMyAccountUserPhoneMutation();
// E.164: + and the country code, then digits. An empty string clears it.
await setPhone({ phone: '+37060000000' }).unwrap();The team list (useGetAccountCollaboratorsQuery) also provides Account, so it
re-reads after a phone change. It now pages with flat startingAfter,
endingBefore and pageSize (default 20), and projectId without
includeAccountOwner lists only that project's collaborators.
Every scheduler Hub endpoint (8) has a hook, 1:1 with norbix.hub.scheduler.
A task runs on a 5-field cron in UTC; only EmailCampaign tasks exist today.
| Area | Hooks | Cache tag |
|---|---|---|
| Module switch | useEnableSchedulerModuleMutation, useDisableSchedulerModuleMutation (PUT on the wire from @norbix.ai/ts 4.6.0) |
invalidates Account, Projects, Scheduler |
| Tasks | useGetSchedulerTasksQuery, useGetSchedulerTaskQuery, useSaveSchedulerTaskMutation, useEnableSchedulerTaskMutation, useDisableSchedulerTaskMutation, useDeleteSchedulerTaskMutation |
provides / invalidates Scheduler/LIST and Scheduler/<id> |
const [save, { isLoading }] = useSaveSchedulerTaskMutation();
// Every Monday 09:00 UTC, send template tpl_123 to all users.
// initiatorUserId is the user the task runs as: you, or a project service user.
await save({
name: 'Weekly digest',
cron: '0 9 * * 1',
initiatorUserId: 'usr_123',
isEnabled: true,
stopOnError: false,
task: {
type: 'EmailCampaign',
campaign: { source: 'AllUsers', templateId: 'tpl_123' },
},
});The typed task body (plain 'EmailCampaign' / 'AllUsers' strings, no
cast) comes from @norbix.ai/ts 4.6.0. Full method reference: the core SDK's
docs/hub/scheduler.md.
Every hub.code endpoint outside the marketplace (15) has a hook, 1:1 with
norbix.hub.code: the module switch, the Code integrations (AWS Lambda, Azure
Functions, Google Cloud Functions credentials) and the cloud functions they
may call — list, aliases / versions, a call by hand, a sample envelope for a
local run, and the last runs of a trigger or task. The five function hooks
need @norbix.ai/ts 4.19.0.
| Area | Hooks | Cache tag |
|---|---|---|
| Module switch | useEnableCodeModuleMutation, useDisableCodeModuleMutation |
invalidates Account, Projects, Code |
| Integrations | useGetCodeIntegrationsQuery, useGetCodeIntegrationQuery, useSaveCodeIntegrationMutation, useDeleteCodeIntegrationMutation, useEnableCodeIntegrationMutation, useDisableCodeIntegrationMutation, useTestCodeIntegrationMutation |
provides / invalidates CodeIntegrations/LIST and CodeIntegrations/<id>; test touches nothing |
| Cloud functions | useGetCodeFunctionsQuery, useGetCodeFunctionQualifiersQuery, useGetCodeFunctionEventSampleQuery, useGetCodeFunctionRunsQuery, useInvokeCodeFunctionMutation |
provides CodeFunctions/LIST, CodeFunctions/SAMPLE, CodeFunctions/RUNS; invoke touches nothing |
const integrationId = 'int_6rBACnEaxQ0x1jTiHtAuWV';
const { data } = useGetCodeFunctionsQuery({ integrationId, prefix: 'orders-' });
const [invoke, { data: result }] = useInvokeCodeFunctionMutation();
await invoke({
functionRef: { integrationId, functionId: 'orders-on-update', qualifier: 'prod' },
payload: { orderId: '6ac54a59212c29912eaddd81', dryRun: true },
invocationType: 'Sync',
});
// result: { statusCode, durationMs, output, requestId, logs }
// The last runs of a trigger (or `{ taskId }` for a scheduled task), newest first.
const { data: runs } = useGetCodeFunctionRunsQuery({
triggerId: 'trg_1p4R3gTobGZpD6fXvHT4rd',
limit: 20,
});The function side — parsing the envelope your function receives — is
parseFunctionEvent in the core SDK (docs/code-functions.md), not a hook.
Full method reference: the core SDK's docs/hub/code.md.
The marketplace methods of hub.code have hooks too, 1:1 with
norbix.hub.code (gateway contract Code.Marketplace.md §14): the catalog of
listings, the installed connections (int_…) with their secrets, token
mappings and validation test, the vendor function catalog, and the functions
added on a connection (func_…). Every tag is of type CodeMarketplace.
| Area | Hooks | Cache tag (CodeMarketplace/…) |
|---|---|---|
| Catalog | useGetMarketplaceListingsQuery, useGetMarketplaceListingQuery, useGetMarketplaceListingFunctionTokensQuery |
provides LISTINGS |
| Connections | useGetMarketplaceIntegrationsQuery, useGetMarketplaceIntegrationQuery, useSaveMarketplaceIntegrationMutation, useDeleteMarketplaceIntegrationMutation, useEnableMarketplaceIntegrationMutation, useDisableMarketplaceIntegrationMutation, useReplaceMarketplaceIntegrationSecretsMutation, useRevealMarketplaceIntegrationSecretsMutation, useSetMarketplaceIntegrationTokenMappingsMutation, useTestMarketplaceIntegrationMutation, useGetMarketplaceFunctionCatalogQuery, useGetMarketplaceIntegrationToolsQuery (the stored MCP tool catalog of an MCP connection, core SDK 4.23.0+) |
INTEGRATIONS, <integrationViewId>, CATALOG:<integrationViewId>, TOOLS:<integrationViewId> (test and delete invalidate it); reveal touches nothing |
| Access | useGetMarketplaceIntegrationAccessQuery (the access matrix of a connection: roles, packages, tools, functions), useSetMarketplaceIntegrationRoleAccessMutation (a role's package + Manage), useSaveMarketplaceIntegrationCustomAccessMutation (a custom policy for one role) — core SDK 4.24.0+ |
ACCESS:<integrationViewId>; the two writes invalidate only that connection's matrix, and so do test, delete, function save and function delete |
| Sign-in | useStartMarketplaceIntegrationSignInMutation (OAuth sign-in of an MCP connection: open authorizeUrl; core SDK 4.24.0+) |
invalidates nothing — read the connection again (signedIn) after the person returns |
| Sign-out | useSignOutMarketplaceIntegrationMutation (OAuth sign-out of an MCP connection: wasSignedIn, revoke; core SDK 4.25.0+) |
invalidates <integrationViewId> and ACCESS:<integrationViewId> of that connection |
| Functions | useGetMarketplaceFunctionsQuery, useGetMarketplaceFunctionQuery, useSaveMarketplaceFunctionMutation, useDeleteMarketplaceFunctionMutation, useEnableMarketplaceFunctionMutation, useDisableMarketplaceFunctionMutation, useGetMarketplaceFunctionTokensQuery, useInvokeMarketplaceFunctionMutation, useGetMarketplaceFunctionRunsQuery |
FUNCTIONS:<integrationViewId>, <functionViewId>, RUNS; invoke touches nothing |
Revealing secrets is a mutation on purpose: a query would keep the clear values in the store's cache.
const { data: listings } = useGetMarketplaceListingsQuery({ search: 'slack' });
const [invoke] = useInvokeMarketplaceFunctionMutation();
await invoke({ functionViewId: 'func_4hT2b9QkLm3XcT1yRfGh7a', payload: { text: 'Order shipped' } });useGetMarketplaceFunctionRunsQuery takes exactly one of functionId (a function's own runs from every source — invoke, trigger, scheduler — newest first, up to 50), triggerId or taskId. A row carries status (ok / failed / skipped), durationMs, source, correlationId and hasDetails; when hasDetails is true, the run's input, steps and response are in the project log — read them with norbix.hub.code.getMarketplaceFunctionRunDetails(row.correlationId) from @norbix.ai/ts 4.22.0.
const { data } = useGetMarketplaceFunctionRunsQuery({
functionId: 'func_4hT2b9QkLm3XcT1yRfGh7a',
limit: 20,
});A taxonomy is a named tree of terms (labels). A term can have one parent (a clean hierarchy) or several parents (the same item under many categories). There is one read hook per scenario — pick the one that matches what you want:
| I want to… | Hook | data holds |
|---|---|---|
| Get a taxonomy's terms as a flat list | useFindTermsQuery |
a paginated list of terms |
| Get only the children of one term | useFindTermsChildrenQuery |
a list of child terms (direct + multi-parent) |
| Get a taxonomy's terms as a ready-made tree | useFindTermTreeQuery |
a tree of nested term nodes |
| Get the taxonomy structure (e.g. Countries → Cities) | useFindTaxonomyTreeQuery |
a tree of taxonomy nodes |
All four share the same cache tag, so a write to a taxonomy refreshes every term view automatically. The examples below all use one example services taxonomy shaped like this:
Indoors
└─ Air conditioning
└─ Wall-mounted
Outdoors
└─ Solar panels
Goal: show every term of services in a simple list, in display order.
const { data, isLoading } = useFindTermsQuery({ taxonomyName: 'services' });{
"list": {
"items": [
{
"id": "term_indoors",
"taxonomyName": "services",
"parentId": null,
"order": 1,
"name": "Indoors"
},
{
"id": "term_air_con",
"taxonomyName": "services",
"parentId": "term_indoors",
"order": 1,
"name": "Air conditioning"
},
{
"id": "term_wall",
"taxonomyName": "services",
"parentId": "term_air_con",
"order": 1,
"name": "Wall-mounted"
},
{
"id": "term_outdoors",
"taxonomyName": "services",
"parentId": null,
"order": 2,
"name": "Outdoors"
},
{
"id": "term_solar",
"taxonomyName": "services",
"parentId": "term_outdoors",
"order": 1,
"name": "Solar panels"
}
],
"hasMore": false,
"hasPrevious": false,
"startingAfter": null,
"endingBefore": null
},
"responseStatus": { "isSuccess": true }
}The list is flat — every term is one row, with its parentId telling you where it sits. The nesting is not built for you here (use useFindTermTreeQuery for that).
Goal: show just the roots (no parent) — for the first level of a menu.
const { data } = useFindTermsQuery({
taxonomyName: 'services',
filter: '{ "parentId": null }',
});{
"list": {
"items": [
{
"id": "term_indoors",
"taxonomyName": "services",
"parentId": null,
"order": 1,
"name": "Indoors"
},
{
"id": "term_outdoors",
"taxonomyName": "services",
"parentId": null,
"order": 2,
"name": "Outdoors"
}
],
"hasMore": false,
"hasPrevious": false,
"startingAfter": null,
"endingBefore": null
},
"responseStatus": { "isSuccess": true }
}filter is an optional MongoDB filter, ANDed with the taxonomy. Use it to fetch one level at a time (lazy tree loading) or to find terms by any field.
Goal: the user expanded Indoors — load what is directly under it.
const { data } = useFindTermsChildrenQuery({
taxonomyName: 'services',
parentId: 'term_indoors',
});{
"list": {
"items": [
{
"id": "term_air_con",
"taxonomyName": "services",
"parentId": "term_indoors",
"order": 1,
"name": "Air conditioning",
"multiParents": [
{ "taxonomyId": "tax_service_types", "parentId": "term_indoors", "name": "Indoors" },
{
"taxonomyId": "tax_service_types",
"parentId": "term_energy_efficient",
"name": "Energy efficient"
}
]
}
],
"hasMore": false,
"hasPrevious": false
},
"responseStatus": { "isSuccess": true }
}This returns both direct children (their parentId is term_indoors) and multi-parent children (terms that list term_indoors in multiParents). Parent names are already resolved, so no second lookup.
Goal: in a products taxonomy, a Relaxing massage oil belongs to For couples, Gift ideas, and Body care. Listing the children of any of those categories returns it.
const { data } = useFindTermsChildrenQuery({
taxonomyName: 'products',
parentId: 'term_gift_ideas',
});{
"list": {
"items": [
{
"id": "term_relaxing_oil",
"taxonomyName": "products",
"name": "Relaxing massage oil",
"multiParents": [
{ "taxonomyId": "tax_categories", "parentId": "term_for_couples", "name": "For couples" },
{ "taxonomyId": "tax_categories", "parentId": "term_gift_ideas", "name": "Gift ideas" },
{ "taxonomyId": "tax_categories", "parentId": "term_body_care", "name": "Body care" }
]
}
],
"hasMore": false,
"hasPrevious": false
},
"responseStatus": { "isSuccess": true }
}One product, three category links — no duplicate listings. The same product would also come back from the children of term_for_couples and term_body_care.
Goal: render the full services tree at once, already nested.
const { data } = useFindTermTreeQuery({ taxonomyName: 'services' });{
"tree": [
{
"id": "term_indoors",
"name": "Indoors",
"order": 1,
"children": [
{
"id": "term_air_con",
"name": "Air conditioning",
"order": 1,
"children": [{ "id": "term_wall", "name": "Wall-mounted", "order": 1, "children": null }]
}
]
},
{
"id": "term_outdoors",
"name": "Outdoors",
"order": 2,
"children": [{ "id": "term_solar", "name": "Solar panels", "order": 1, "children": null }]
}
],
"responseStatus": { "isSuccess": true }
}Roots are in tree; each node carries its own children; a leaf has children: null. The tree arrives ready to render — no client-side tree building.
Goal: start from Indoors and go at most 2 levels deep.
const { data } = useFindTermTreeQuery({
taxonomyName: 'services',
rootTermId: 'term_indoors',
depth: 2,
});{
"tree": [
{
"id": "term_indoors",
"name": "Indoors",
"order": 1,
"children": [
{ "id": "term_air_con", "name": "Air conditioning", "order": 1, "children": null }
]
}
],
"responseStatus": { "isSuccess": true }
}With depth: 2 you get Indoors (level 1) and Air conditioning (level 2); Wall-mounted (level 3) is cut off, so Air conditioning shows children: null.
Goal: see how taxonomies relate to each other (e.g. a Cities taxonomy whose parent is Countries), structure only.
const { data } = useFindTaxonomyTreeQuery({});{
"tree": [
{
"viewId": "txn_countries",
"taxonomyName": "Countries",
"taxonomySlug": "countries",
"parentId": null,
"children": [
{
"viewId": "txn_cities",
"taxonomyName": "Cities",
"taxonomySlug": "cities",
"parentId": "txn_countries",
"children": null,
"terms": null
}
],
"terms": null
}
],
"responseStatus": { "isSuccess": true }
}This is the taxonomy tree, not the term tree: nodes are taxonomies. Every terms is null because we did not ask for terms.
Goal: same structure, but also pull each taxonomy's terms in the same call.
const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });{
"tree": [
{
"viewId": "txn_countries",
"taxonomyName": "Countries",
"taxonomySlug": "countries",
"parentId": null,
"terms": [
{ "id": "term_lt", "name": "Lithuania", "order": 1, "children": null },
{ "id": "term_lv", "name": "Latvia", "order": 2, "children": null }
],
"children": [
{
"viewId": "txn_cities",
"taxonomyName": "Cities",
"taxonomySlug": "cities",
"parentId": "txn_countries",
"terms": [
{ "id": "term_vilnius", "name": "Vilnius", "order": 1, "children": null },
{ "id": "term_kaunas", "name": "Kaunas", "order": 2, "children": null }
],
"children": null
}
]
}
],
"responseStatus": { "isSuccess": true }
}Now each taxonomy node's terms holds that taxonomy's full term tree (same shape as useFindTermTreeQuery) — Countries carries its countries, Cities carries its cities.
All four hooks also accept the usual RTK Query options (e.g.
{ skip: !ready }), and an optionaldatabaseIntegrationIdin the argument to target a non-default database.
const projectId = useSelector(selectCurrentProjectId);
const { data } = useGetProjectQuery({ id: projectId }, { skip: !projectId });const dispatch = useDispatch();
const [login] = useLoginMutation();
async function handleLogin(creds) {
await login(creds).unwrap();
// The login mutation already invalidates the most-affected tags.
// For a hard reset across every cached endpoint, also do:
dispatch(norbixApi.util.resetApiState());
}function ApiVersionBadge() {
const norbix = useNorbix();
const [v, setV] = useState<string>();
useEffect(() => {
norbix.api.echo.echo({}).then((r) => setV(r.gatewayVersion));
}, [norbix]);
return <small>API {v}</small>;
}const tenantNorbix = useMemo(() => norbix.with({ projectId, accountId }), [projectId, accountId]);
return (
<NorbixProvider client={tenantNorbix}>
<Workspace />
</NorbixProvider>
);For RTK Query to follow tenant changes, recreate the API slice or pass a tenant-aware getClient resolver to createNorbixApi.
The language is a client setting: the hooks send whatever the wrapped core client sends. Set it with the provider prop (or norbix.setLanguage(...)); Accept-Language is the only language input (the old cultureCode field and CultureCode / norbix-language headers are gone).
<NorbixProvider client={norbix} language="lt-LT, en;q=0.8">
<App />
</NorbixProvider>;
// switching later: change the prop (or call norbix.setLanguage('en')), then drop
// results read in the old language
dispatch(norbixApi.util.resetApiState());Read every translation instead of one language with allLanguages: true on record and term reads, e.g. useFindCollectionQuery({ collectionName: 'articles', allLanguages: true }).
| Hook | SDK call | Tags |
|---|---|---|
useRegisterPushDeviceMutation({ provider, token, platform, … }) |
registerDevice |
invalidates PushDevices |
useListPushDevicesQuery() |
listDevices |
provides PushDevices |
useRemovePushDeviceMutation(id) |
removeDevice |
invalidates PushDevices |
useRemovePushDeviceByTokenMutation(token) |
removeDeviceByToken |
invalidates PushDevices |
useListPushMessagesQuery({ unreadOnly, pageSize }) |
listMessages |
provides PushMessages |
useMarkPushMessageReadMutation(id) |
markMessageRead |
invalidates PushMessages |
useMarkAllPushMessagesReadMutation() |
markAllMessagesRead |
invalidates PushMessages |
const [register] = norbixApi.useRegisterPushDeviceMutation();
await register({
provider: 'expo',
token: expoPushToken,
platform: 'ios',
installationId,
}).unwrap();
const { data } = norbixApi.useListPushMessagesQuery({ unreadOnly: true });A Norbix project spans one primary region plus any number of additional regions. Two concerns, two homes: which region your requests target is configured on the wrapped norbix client (this package inherits it — every hook just calls the client you passed to createNorbixApi), while which regions a project spans is managed through the two hooks below.
Configure the target region on the wrapped client.
// 1. At construction
const norbix = new Norbix({ region: 'nb-eu-germany' });
// 2. Via environment — `new Norbix()` picks it up automatically
// NORBIX_REGION=nb-eu-germany
// 3. At runtime
norbix.setRegion('nb-eu-germany'); // all subsequent requests
norbix.setRegion(undefined); // clear — the backend uses the project's primary regionWhen a region is set, every request carries the nb-region header, and the client itself composes the regional base URL (https://nb-eu-germany.api.norbix.ai) — but only when it is using the SDK's default base URLs; a custom baseUrl is never rewritten. There is no default region: with nothing set, no header is sent and the backend picks the project's primary region. The underlying SDK also accepts a per-call override (norbix.hub.regions.list({}, { region: 'nb-eu-germany' })); the shipped hooks don't expose that option, so for a one-off cross-region call drop down to the SDK via useNorbix().
Switching regions does not refetch by itself. Same caveat as the tenant switch above: RTK Query keys its cache by endpoint + args, and the region is part of neither. After norbix.setRegion(...), data cached from the old region stays in the store until something invalidates it. Reset the cache the same way you would after a tenant switch:
norbix.setRegion('nb-eu-germany');
dispatch(norbixApi.util.resetApiState()); // hard reset of every cached endpoint
// or, surgically:
dispatch(norbixApi.util.invalidateTags(['Regions', 'Projects']));Manage the regions a project spans.
import { useListRegionsQuery, useUpdateProjectRegionsMutation } from './norbix';
function RegionSettings({ projectId }: { projectId: string }) {
// GET /account/regions — the regions available to the account
const { data, isLoading } = useListRegionsQuery({});
// PATCH /account/projects/{projectId}/settings/regions
const [updateRegions, { isLoading: saving }] = useUpdateProjectRegionsMutation();
if (isLoading) return <p>Loading…</p>;
return (
<select
disabled={saving}
onChange={(e) =>
updateRegions({
projectId,
primaryRegion: e.target.value, // a region code, e.g. "nb-eu-germany"
additionalRegions: [],
})
}
>
{data?.items?.map((r) => (
<option key={r.id} value={r.id}>
{r.name ?? r.id}
</option>
))}
</select>
);
}useListRegionsQuerywrapsnorbix.hub.regions.listand provides theRegionstag. As with every query hook, a lazy variant —useLazyListRegionsQuery— is generated alongside it.useUpdateProjectRegionsMutationwrapsnorbix.hub.regions.updateProjectRegions({ projectId, primaryRegion?, additionalRegions? })and invalidatesRegionsandProjects— changing the regions a project spans updates the project DTO (primaryRegion/additionalRegions), so cached region lists and project queries refetch automatically once the mutation resolves.
hub.accountcarriesgetAccountRegions/updateProjectRegionsaliases for the same wire endpoints (useGetAccountRegionsQuerytagsAccount). Thehub.regionshooks above are the canonical pair — they are the ones wired to theRegionstag.
Almost every Hub module exposes the same integrations CRUD surface (getXIntegrations, saveXIntegration, enableXIntegration, ...). Instead of copy-pasting ~50 lines per module, use buildIntegrationsEndpoints. The package already uses it for hub.database; wire the other 8 in your app via injectEndpoints:
import { norbixApi, buildIntegrationsEndpoints } from '@norbix.ai/react-redux';
norbixApi.injectEndpoints({
endpoints: (b) => ({
// Email integrations live on hub.notifications (not hub.email)
...buildIntegrationsEndpoints(b, {
prefix: 'Email',
tag: 'EmailIntegrations',
namespace: (n) => n.hub.notifications,
include: { test: true, confirmHumanDelivery: true },
}),
// Push integrations
...buildIntegrationsEndpoints(b, {
prefix: 'Push',
tag: 'PushIntegrations',
namespace: (n) => n.hub.notifications,
include: { test: true, confirmHumanDelivery: true },
}),
// Sms integrations
...buildIntegrationsEndpoints(b, {
prefix: 'Sms',
tag: 'SmsIntegrations',
namespace: (n) => n.hub.notifications,
include: { test: true, confirmHumanDelivery: true },
}),
// Files integrations
...buildIntegrationsEndpoints(b, {
prefix: 'Files',
tag: 'FilesIntegrations',
namespace: (n) => n.hub.files,
include: { test: true },
}),
// Payments integrations
...buildIntegrationsEndpoints(b, {
prefix: 'Payments',
tag: 'PaymentIntegrations',
namespace: (n) => n.hub.payments,
include: { test: true, confirmHumanDelivery: true },
}),
// Code integrations
...buildIntegrationsEndpoints(b, {
prefix: 'Code',
tag: 'CodeIntegrations',
namespace: (n) => n.hub.code ?? (n.hub as never),
include: { test: true },
}),
// Membership integrations
...buildIntegrationsEndpoints(b, {
prefix: 'Membership',
tag: 'MembershipIntegrations',
namespace: (n) => n.hub.membership,
include: { test: false },
}),
// Logs integrations — uses the `Logging` SDK prefix, not `Logs`
...buildIntegrationsEndpoints(b, {
prefix: 'Logging',
tag: 'LogsIntegrations',
namespace: (n) => n.hub.logs,
include: { test: true },
}),
}),
overrideExisting: false,
});This produces all hooks automatically — useGetEmailIntegrationsQuery, useSaveEmailIntegrationMutation, useTestEmailIntegrationMutation, useConfirmEmailIntegrationHumanDeliveryMutation, and the same for push/sms/files/payments/etc.
| Module | Prefix | Namespace | Tag | Notes |
|---|---|---|---|---|
Email |
n.hub.notifications |
EmailIntegrations |
has test + confirmHumanDelivery |
|
| Push | Push |
n.hub.notifications |
PushIntegrations |
has test + confirmHumanDelivery |
| Sms | Sms |
n.hub.notifications |
SmsIntegrations |
has test + confirmHumanDelivery |
| Files | Files |
n.hub.files |
FilesIntegrations |
has test |
| Payments | Payments |
n.hub.payments |
PaymentIntegrations |
has test + confirmHumanDelivery |
| Code | Code |
n.hub.code |
CodeIntegrations |
has test |
| Membership | Membership |
n.hub.membership |
MembershipIntegrations |
no test |
| Logs | Logging (uses Logging, not Logs) |
n.hub.logs |
LogsIntegrations |
has test (deliveries start after a passed test) |
| Database | Database |
n.hub.database |
DatabaseIntegrations |
already wired in package |
Trade-off. The helper is terse but loses static request/response typing — generated hooks return any data, because endpoint keys are computed at runtime. If you need full types on a specific integration surface (autocompletion in your IDE), define those endpoints manually instead, following the canonical shape used by hub.database integrations in src/hooks/hub/database.ts.
The package ships a curated set of hooks. When your app needs an endpoint we don't expose, you can add it without forking — createNorbixApi returns the standard RTK Query API object, so injectEndpoints works exactly as documented in the RTK Query docs:
// src/services/myCampaigns.ts
import { norbixApi } from '../norbix';
import type { useNorbix } from '@norbix.ai/react-redux';
export const myCampaignsService = norbixApi.injectEndpoints({
endpoints: (builder) => ({
getEmailCampaigns: builder.query({
query: (args) => (norbix) =>
norbix.hub.notifications
? norbix.hub.notifications.getEmailTemplates(args) // example only
: Promise.resolve({}),
providesTags: [{ type: 'EmailCampaigns', id: 'LIST' }],
}),
// ...more app-specific endpoints
}),
overrideExisting: false,
});
export const { useGetEmailCampaignsQuery } = myCampaignsService;The injected endpoints share the same baseQuery, the same tag taxonomy, and the same Redux slice — they integrate fully with cache invalidation across the package's hooks and your own.
Norbix gateway responses are envelopes — { list: { items: [...] } }, { user: {...} }, etc. The hooks return the envelope shape (matching the SDK return type). To get just the inner array or item in a component, use selectFromResult:
const { users, isLoading } = useGetUsersQuery(args, {
selectFromResult: ({ data, isLoading }) => ({
users: data?.list?.result ?? [],
isLoading,
}),
});Or unwrap once at the endpoint level via transformResponse (use injectEndpoints to add a project-specific variant):
norbixApi.injectEndpoints({
endpoints: (builder) => ({
getUsersList: builder.query<UserDto[], Partial<GetUsersRequest>>({
query: (args) => (norbix) => norbix.api.membership.getUsers(args),
transformResponse: (res) => res.list?.result ?? [],
providesTags: [{ type: 'MembershipUsers', id: 'LIST' }],
}),
}),
overrideExisting: false,
});If your app currently calls Norbix via fetchBaseQuery({ baseUrl }) + a custom prepareHeaders, you're reinventing what the SDK already does (auth precedence, retries, error mapping, env loading, MCP-aligned shape). Migration sketch:
- Replace
fetchBaseQuerysetup withcreateNorbixApi(() => norbix). - Mount
<NorbixProvider client={norbix}>once at app root, alongside the existing<Provider store={store}>. - Migrate per-service file: rewrite each
api.injectEndpoints({ endpoints: builder => ({ getX: builder.query(...) }) })fromurl + method + bodyto thequery: (args) => (norbix) => norbix.api.<module>.<method>(args)shape. Keep the sameprovidesTags/invalidatesTags. - Remove the custom error interceptor —
createNorbixBaseQueryalready mapsNorbixErrortoSerializedNorbixErrorwithcode,status,message,fieldErrors. Consume those in your error UI instead. - Keep the same tag taxonomy — the package exports the full set (
Account,Projects,Regions,MembershipUsers,DatabaseSchemas, ... 59 in total), so existingprovidesTagskeep working.
A failed call lands in the Redux state as a plain SerializedNorbixError:
const { error } = useGetFileInfoQuery({ filesIntegrationId, path });
if (error) {
// httpStatus / errorCode / errors are the names every Norbix SDK uses.
// status / code / fieldErrors are the same values, kept for older code.
console.log(error.httpStatus, error.errorCode, error.message);
error.errors.forEach((e) => console.log(e.errorCode, e.fieldName, e.message));
}The message and the error code are the gateway's own. @norbix.ai/ts reads
them out of responseStatus.errors[], where the gateway puts them, and this
package copies them into the store unchanged — it does not parse bodies
itself.
The gateway answers a business refusal (an unknown id, a rule that says no)
with HTTP 200 and responseStatus.isSuccess = false. Until now such an
answer arrived as { data } and your components rendered it as a success.
With the fixed @norbix.ai/ts it arrives as { error } with httpStatus 200
and the gateway's message and error code.
If a component checked data.responseStatus.isSuccess itself, read error
instead. This needs @norbix.ai/ts 2.0.0 or newer — the version that
carries the fix.
createNorbixApi(getClient) returns an RTK Query API. Its baseQuery is a thin wrapper that calls a closure you pass at endpoint definition time:
// inside @norbix.ai/react-redux
const baseQuery = async (call) => {
try {
return { data: await call(getClient()) };
} catch (err) {
return { error: serializeNorbixError(err) };
}
};Every endpoint is shaped like:
getUsers: b.query({
query: (args) => (norbix) => norbix.api.membership.getUsers(args),
providesTags: [{ type: 'User', id: 'LIST' }],
}),The closure carries the args and runs against the live SDK. Types come from the SDK's own method signatures via small Result<F> / Arg<F> helpers — no DTO imports required.
npm install
npm run lint
npm run typecheck
npm test
npm run buildConventional commits are required. Pushes to main are released to npm by semantic-release with provenance enabled. next and beta branches publish prereleases.
MIT © Norbix